mirror of
https://github.com/JakubMelka/PDF4QT.git
synced 2025-02-28 17:37:46 +01:00
ImageMask 1-bit images
This commit is contained in:
parent
630afbba61
commit
84f26180c5
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user