ImageMask 1-bit images

This commit is contained in:
Jakub Melka 2019-06-15 17:40:22 +02:00
parent 630afbba61
commit 84f26180c5
6 changed files with 137 additions and 31 deletions

View File

@ -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)); 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<PDFReal>& 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<QByteArray*>(&imageData.getData()), QIODevice::ReadOnly); QDataStream stream(const_cast<QByteArray*>(&imageData.getData()), QIODevice::ReadOnly);
PDFBitReader reader(&stream, imageData.getBitsPerComponent()); PDFBitReader reader(&stream, imageData.getBitsPerComponent());
PDFColor color; PDFColor color;
color.resize(componentCount); 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) for (unsigned int i = 0, rowCount = imageData.getHeight(); i < rowCount; ++i)
{ {
reader.seek(i * imageData.getStride()); reader.seek(i * imageData.getStride());
@ -119,8 +126,17 @@ QImage PDFAbstractColorSpace::getImage(const PDFImageData& imageData) const
{ {
for (unsigned int k = 0; k < componentCount; ++k) for (unsigned int k = 0; k < componentCount; ++k)
{ {
PDFBitReader::Value value = reader.read(); PDFReal 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;
}
} }
QColor transformedColor = getColor(color); QColor transformedColor = getColor(color);
@ -147,19 +163,26 @@ QImage PDFAbstractColorSpace::getImage(const PDFImageData& imageData) const
} }
Q_ASSERT(componentCount > 0); Q_ASSERT(componentCount > 0);
std::vector<PDFInteger> colorKeyMask = imageData.getColorKeyMask(); const std::vector<PDFInteger>& colorKeyMask = imageData.getColorKeyMask();
if (colorKeyMask.size() / 2 != componentCount) 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())); 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<PDFReal>& 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<QByteArray*>(&imageData.getData()), QIODevice::ReadOnly); QDataStream stream(const_cast<QByteArray*>(&imageData.getData()), QIODevice::ReadOnly);
PDFBitReader reader(&stream, imageData.getBitsPerComponent()); PDFBitReader reader(&stream, imageData.getBitsPerComponent());
PDFColor color; PDFColor color;
color.resize(componentCount); 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) for (unsigned int i = 0, rowCount = imageData.getHeight(); i < rowCount; ++i)
{ {
reader.seek(i * imageData.getStride()); reader.seek(i * imageData.getStride());
@ -173,11 +196,20 @@ QImage PDFAbstractColorSpace::getImage(const PDFImageData& imageData) const
for (unsigned int k = 0; k < componentCount; ++k) for (unsigned int k = 0; k < componentCount; ++k)
{ {
PDFBitReader::Value value = reader.read(); 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()); Q_ASSERT(2 * k + 1 < colorKeyMask.size());
if (static_cast<decltype(colorKeyMask)::value_type>(value) >= colorKeyMask[2 * k] && if (static_cast<std::decay<decltype(colorKeyMask)>::type::value_type>(value) >= colorKeyMask[2 * k] &&
static_cast<decltype(colorKeyMask)::value_type>(value) <= colorKeyMask[2 * k + 1]) static_cast<std::decay<decltype(colorKeyMask)>::type::value_type>(value) <= colorKeyMask[2 * k + 1])
{ {
++maskedColors; ++maskedColors;
} }

View File

@ -80,8 +80,9 @@ public:
enum class MaskingType enum class MaskingType
{ {
None, None,
ColorKeyMasking, ColorKeyMasking, ///< Masking by color key
ImageMasking 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() : explicit PDFImageData() :
@ -102,7 +103,8 @@ public:
unsigned int stride, unsigned int stride,
MaskingType maskingType, MaskingType maskingType,
QByteArray data, QByteArray data,
std::vector<PDFInteger>&& colorKeyMask) : std::vector<PDFInteger>&& colorKeyMask,
std::vector<PDFReal>&& decode) :
m_components(components), m_components(components),
m_bitsPerComponent(bitsPerComponent), m_bitsPerComponent(bitsPerComponent),
m_width(width), m_width(width),
@ -110,7 +112,8 @@ public:
m_stride(stride), m_stride(stride),
m_maskingType(maskingType), m_maskingType(maskingType),
m_data(qMove(data)), 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; } unsigned int getStride() const { return m_stride; }
MaskingType getMaskingType() const { return m_maskingType; } MaskingType getMaskingType() const { return m_maskingType; }
const QByteArray& getData() const { return m_data; } const QByteArray& getData() const { return m_data; }
std::vector<PDFInteger> getColorKeyMask() const { return m_colorKeyMask; } const std::vector<PDFInteger>& getColorKeyMask() const { return m_colorKeyMask; }
const std::vector<PDFReal>& getDecode() const { return m_decode; }
/// Returns number of color channels /// Returns number of color channels
unsigned int getColorChannels() const { return m_components; } 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, /// 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 ]. /// consisting of [ min_0, max_0, min_1, max_1, ... , min_n, max_n ].
std::vector<PDFInteger> m_colorKeyMask; std::vector<PDFInteger> 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<PDFReal> m_decode;
}; };
using PDFColor3 = std::array<PDFColorComponent, 3>; using PDFColor3 = std::array<PDFColorComponent, 3>;

View File

@ -20,6 +20,7 @@
#include "pdfparser.h" #include "pdfparser.h"
#include "pdfdocument.h" #include "pdfdocument.h"
#include "pdfexception.h" #include "pdfexception.h"
#include "pdfutils.h"
#include <stack> #include <stack>
#include <iterator> #include <iterator>

View File

@ -110,17 +110,6 @@ protected:
/// \param value Value to be clamped /// \param value Value to be clamped
inline PDFReal clampOutput(size_t index, PDFReal value) const { return qBound<PDFReal>(m_range[2 * index], value, m_range[2 * index + 1]); } inline PDFReal clampOutput(size_t index, PDFReal value) const { return qBound<PDFReal>(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, /// 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]. /// then the function succeeds, and returns value outside of interval [c0, c1].
/// \param x Value to be interpolated /// \param x Value to be interpolated

View File

@ -19,6 +19,7 @@
#include "pdfdocument.h" #include "pdfdocument.h"
#include "pdfconstants.h" #include "pdfconstants.h"
#include "pdfexception.h" #include "pdfexception.h"
#include "pdfutils.h"
#include <openjpeg.h> #include <openjpeg.h>
#include <jpeglib.h> #include <jpeglib.h>
@ -57,12 +58,10 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str
throw PDFParserException(PDFTranslationContext::tr("Image has not data.")); throw PDFParserException(PDFTranslationContext::tr("Image has not data."));
} }
// TODO: Implement ImageMask
// TODO: Implement Decode
// TODO: Implement SMask // TODO: Implement SMask
// TODO: Implement SMaskInData // TODO: Implement SMaskInData
for (const char* notImplementedKey : { "ImageMask", "Decode", "SMask", "SMaskInData" }) for (const char* notImplementedKey : { "SMask", "SMaskInData" })
{ {
if (dictionary->hasKey(notImplementedKey)) if (dictionary->hasKey(notImplementedKey))
{ {
@ -72,6 +71,8 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str
PDFImageData::MaskingType maskingType = PDFImageData::MaskingType::None; PDFImageData::MaskingType maskingType = PDFImageData::MaskingType::None;
std::vector<PDFInteger> mask; std::vector<PDFInteger> mask;
std::vector<PDFReal> decode = loader.readNumberArrayFromDictionary(dictionary, "Decode");
bool imageMask = loader.readBooleanFromDictionary(dictionary, "ImageMask", false);
// Fill Mask // Fill Mask
if (dictionary->hasKey("Mask")) if (dictionary->hasKey("Mask"))
@ -85,11 +86,16 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str
else if (object.isStream()) else if (object.isStream())
{ {
// TODO: Implement Mask Image // TODO: Implement Mask Image
maskingType = PDFImageData::MaskingType::ImageMasking; maskingType = PDFImageData::MaskingType::Image;
throw PDFRendererException(RenderErrorType::NotImplemented, PDFTranslationContext::tr("Mask image is not implemented.")); throw PDFRendererException(RenderErrorType::NotImplemented, PDFTranslationContext::tr("Mask image is not implemented."));
} }
} }
if (imageMask)
{
maskingType = PDFImageData::MaskingType::ImageMask;
}
// Retrieve filters // Retrieve filters
PDFObject filters; PDFObject filters;
if (dictionary->hasKey(PDF_STREAM_DICT_FILTER)) if (dictionary->hasKey(PDF_STREAM_DICT_FILTER))
@ -245,7 +251,7 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str
} }
jpeg_finish_decompress(&codec); 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); 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(); valid = image.m_imageData.isValid();
} }
else else
@ -477,7 +483,32 @@ PDFImage PDFImage::createImage(const PDFDocument* document, const PDFStream* str
const unsigned int stride = (components * bitsPerComponent * width + 7) / 8; const unsigned int stride = (components * bitsPerComponent * width + 7) / 8;
QByteArray imageDataBuffer = document->getDecodedStream(stream); 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<unsigned int>(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<unsigned int>(loader.readIntegerFromDictionary(dictionary, "Width", 0));
const unsigned int height = static_cast<unsigned int>(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; return image;
@ -489,6 +520,39 @@ QImage PDFImage::getImage() const
{ {
return m_colorSpace->getImage(m_imageData); 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<QByteArray*>(&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<bool>(reader.read());
*outputLine++ = transparent ? 0x00 : 0xFF;
}
}
return image;
}
return QImage(); return QImage();
} }

View File

@ -99,6 +99,17 @@ private:
Value m_bitsInBuffer; 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 } // namespace pdf
#endif // PDFUTILS_H #endif // PDFUTILS_H