2008-04-29 12:55:25 +00:00
|
|
|
#include <qpdf/QPDF_Stream.hh>
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
#include <qpdf/ContentNormalizer.hh>
|
2008-04-29 12:55:25 +00:00
|
|
|
#include <qpdf/Pipeline.hh>
|
2022-05-07 15:12:15 +00:00
|
|
|
#include <qpdf/Pl_Base64.hh>
|
2008-04-29 12:55:25 +00:00
|
|
|
#include <qpdf/Pl_Buffer.hh>
|
2010-08-05 19:04:22 +00:00
|
|
|
#include <qpdf/Pl_Count.hh>
|
2022-05-07 15:12:15 +00:00
|
|
|
#include <qpdf/Pl_Discard.hh>
|
2022-04-02 21:14:10 +00:00
|
|
|
#include <qpdf/Pl_Flate.hh>
|
2008-04-29 12:55:25 +00:00
|
|
|
#include <qpdf/Pl_QPDFTokenizer.hh>
|
2019-06-21 03:35:23 +00:00
|
|
|
#include <qpdf/QIntC.hh>
|
2022-04-02 21:14:10 +00:00
|
|
|
#include <qpdf/QPDF.hh>
|
|
|
|
#include <qpdf/QPDFExc.hh>
|
|
|
|
#include <qpdf/QTC.hh>
|
|
|
|
#include <qpdf/QUtil.hh>
|
2020-12-23 11:12:49 +00:00
|
|
|
#include <qpdf/SF_ASCII85Decode.hh>
|
|
|
|
#include <qpdf/SF_ASCIIHexDecode.hh>
|
2022-04-02 21:14:10 +00:00
|
|
|
#include <qpdf/SF_DCTDecode.hh>
|
|
|
|
#include <qpdf/SF_FlateLzwDecode.hh>
|
|
|
|
#include <qpdf/SF_RunLengthDecode.hh>
|
2008-04-29 12:55:25 +00:00
|
|
|
|
2009-09-26 18:36:04 +00:00
|
|
|
#include <stdexcept>
|
|
|
|
|
2022-04-16 17:21:57 +00:00
|
|
|
namespace
|
2020-12-23 11:12:49 +00:00
|
|
|
{
|
2022-04-16 17:21:57 +00:00
|
|
|
class SF_Crypt: public QPDFStreamFilter
|
2020-12-23 11:12:49 +00:00
|
|
|
{
|
2022-04-16 17:21:57 +00:00
|
|
|
public:
|
|
|
|
SF_Crypt() = default;
|
|
|
|
virtual ~SF_Crypt() = default;
|
|
|
|
|
|
|
|
virtual bool
|
|
|
|
setDecodeParms(QPDFObjectHandle decode_parms)
|
|
|
|
{
|
|
|
|
if (decode_parms.isNull()) {
|
|
|
|
return true;
|
2020-12-23 11:12:49 +00:00
|
|
|
}
|
2022-04-16 17:21:57 +00:00
|
|
|
bool filterable = true;
|
2022-04-30 13:43:07 +00:00
|
|
|
for (auto const& key: decode_parms.getKeys()) {
|
2022-04-16 17:21:57 +00:00
|
|
|
if (((key == "/Type") || (key == "/Name")) &&
|
|
|
|
((!decode_parms.hasKey("/Type")) ||
|
|
|
|
decode_parms.isDictionaryOfType(
|
|
|
|
"/CryptFilterDecodeParms"))) {
|
|
|
|
// we handle this in decryptStream
|
|
|
|
} else {
|
|
|
|
filterable = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return filterable;
|
2020-12-23 11:12:49 +00:00
|
|
|
}
|
|
|
|
|
2022-04-16 17:21:57 +00:00
|
|
|
virtual Pipeline*
|
|
|
|
getDecodePipeline(Pipeline*)
|
|
|
|
{
|
|
|
|
// Not used -- handled by pipeStreamData
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
};
|
2022-05-07 15:12:15 +00:00
|
|
|
|
|
|
|
class StreamBlobProvider
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
StreamBlobProvider(
|
|
|
|
QPDF_Stream* stream, qpdf_stream_decode_level_e decode_level);
|
|
|
|
void operator()(Pipeline*);
|
|
|
|
|
|
|
|
private:
|
|
|
|
QPDF_Stream* stream;
|
|
|
|
qpdf_stream_decode_level_e decode_level;
|
|
|
|
};
|
2022-04-16 17:21:57 +00:00
|
|
|
} // namespace
|
2020-12-23 11:12:49 +00:00
|
|
|
|
|
|
|
std::map<std::string, std::string> QPDF_Stream::filter_abbreviations = {
|
|
|
|
// The PDF specification provides these filter abbreviations for
|
|
|
|
// use in inline images, but according to table H.1 in the pre-ISO
|
|
|
|
// versions of the PDF specification, Adobe Reader also accepts
|
|
|
|
// them for stream filters.
|
|
|
|
{"/AHx", "/ASCIIHexDecode"},
|
|
|
|
{"/A85", "/ASCII85Decode"},
|
|
|
|
{"/LZW", "/LZWDecode"},
|
|
|
|
{"/Fl", "/FlateDecode"},
|
|
|
|
{"/RL", "/RunLengthDecode"},
|
|
|
|
{"/CCF", "/CCITTFaxDecode"},
|
|
|
|
{"/DCT", "/DCTDecode"},
|
|
|
|
};
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
std::map<std::string, std::function<std::shared_ptr<QPDFStreamFilter>()>>
|
|
|
|
QPDF_Stream::filter_factories = {
|
|
|
|
{"/Crypt", []() { return std::make_shared<SF_Crypt>(); }},
|
|
|
|
{"/FlateDecode", SF_FlateLzwDecode::flate_factory},
|
|
|
|
{"/LZWDecode", SF_FlateLzwDecode::lzw_factory},
|
|
|
|
{"/RunLengthDecode", SF_RunLengthDecode::factory},
|
|
|
|
{"/DCTDecode", SF_DCTDecode::factory},
|
|
|
|
{"/ASCII85Decode", SF_ASCII85Decode::factory},
|
|
|
|
{"/ASCIIHexDecode", SF_ASCIIHexDecode::factory},
|
2020-12-23 11:12:49 +00:00
|
|
|
};
|
2010-09-05 15:00:44 +00:00
|
|
|
|
2022-05-07 15:12:15 +00:00
|
|
|
StreamBlobProvider::StreamBlobProvider(
|
|
|
|
QPDF_Stream* stream, qpdf_stream_decode_level_e decode_level) :
|
|
|
|
stream(stream),
|
|
|
|
decode_level(decode_level)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
StreamBlobProvider::operator()(Pipeline* p)
|
|
|
|
{
|
|
|
|
this->stream->pipeStreamData(p, nullptr, 0, decode_level, false, false);
|
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
QPDF_Stream::QPDF_Stream(
|
|
|
|
QPDF* qpdf,
|
|
|
|
int objid,
|
|
|
|
int generation,
|
|
|
|
QPDFObjectHandle stream_dict,
|
|
|
|
qpdf_offset_t offset,
|
|
|
|
size_t length) :
|
2008-04-29 12:55:25 +00:00
|
|
|
qpdf(qpdf),
|
|
|
|
objid(objid),
|
|
|
|
generation(generation),
|
2020-12-27 00:45:01 +00:00
|
|
|
filter_on_write(true),
|
2008-04-29 12:55:25 +00:00
|
|
|
stream_dict(stream_dict),
|
|
|
|
offset(offset),
|
|
|
|
length(length)
|
|
|
|
{
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!stream_dict.isDictionary()) {
|
|
|
|
throw std::logic_error("stream object instantiated with non-dictionary "
|
|
|
|
"object for dictionary");
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
2022-06-25 10:46:47 +00:00
|
|
|
setDescription(
|
|
|
|
this->qpdf,
|
|
|
|
this->qpdf->getFilename() + ", stream object " +
|
|
|
|
QUtil::int_to_string(this->objid) + " " +
|
|
|
|
QUtil::int_to_string(this->generation));
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2022-06-16 16:45:04 +00:00
|
|
|
std::shared_ptr<QPDFObject>
|
|
|
|
QPDF_Stream::create(
|
|
|
|
QPDF* qpdf,
|
|
|
|
int objid,
|
|
|
|
int generation,
|
|
|
|
QPDFObjectHandle stream_dict,
|
|
|
|
qpdf_offset_t offset,
|
|
|
|
size_t length)
|
|
|
|
{
|
|
|
|
return do_create(
|
|
|
|
new QPDF_Stream(qpdf, objid, generation, stream_dict, offset,length));
|
|
|
|
}
|
|
|
|
|
|
|
|
std::shared_ptr<QPDFObject>
|
|
|
|
QPDF_Stream::shallowCopy()
|
|
|
|
{
|
|
|
|
throw std::logic_error("stream objects cannot be cloned");
|
|
|
|
}
|
|
|
|
|
2020-12-23 11:12:49 +00:00
|
|
|
void
|
|
|
|
QPDF_Stream::registerStreamFilter(
|
|
|
|
std::string const& filter_name,
|
|
|
|
std::function<std::shared_ptr<QPDFStreamFilter>()> factory)
|
|
|
|
{
|
|
|
|
filter_factories[filter_name] = factory;
|
|
|
|
}
|
|
|
|
|
2020-12-27 00:45:01 +00:00
|
|
|
void
|
|
|
|
QPDF_Stream::setFilterOnWrite(bool val)
|
|
|
|
{
|
|
|
|
this->filter_on_write = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
QPDF_Stream::getFilterOnWrite() const
|
|
|
|
{
|
|
|
|
return this->filter_on_write;
|
|
|
|
}
|
|
|
|
|
2017-08-28 21:05:34 +00:00
|
|
|
void
|
|
|
|
QPDF_Stream::releaseResolved()
|
|
|
|
{
|
|
|
|
this->stream_provider = 0;
|
|
|
|
QPDFObjectHandle::ReleaseResolver::releaseResolved(this->stream_dict);
|
|
|
|
}
|
|
|
|
|
2010-08-05 20:20:52 +00:00
|
|
|
void
|
|
|
|
QPDF_Stream::setObjGen(int objid, int generation)
|
|
|
|
{
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!((this->objid == 0) && (this->generation == 0))) {
|
2022-02-08 14:18:08 +00:00
|
|
|
throw std::logic_error(
|
|
|
|
"attempt to set object ID and generation of a stream"
|
|
|
|
" that already has them");
|
2010-08-05 20:20:52 +00:00
|
|
|
}
|
|
|
|
this->objid = objid;
|
|
|
|
this->generation = generation;
|
|
|
|
}
|
|
|
|
|
2008-04-29 12:55:25 +00:00
|
|
|
std::string
|
|
|
|
QPDF_Stream::unparse()
|
|
|
|
{
|
|
|
|
// Unparse stream objects as indirect references
|
|
|
|
return QUtil::int_to_string(this->objid) + " " +
|
2022-02-08 14:18:08 +00:00
|
|
|
QUtil::int_to_string(this->generation) + " R";
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2018-12-17 22:40:29 +00:00
|
|
|
JSON
|
2022-05-07 11:53:45 +00:00
|
|
|
QPDF_Stream::getJSON(int json_version)
|
2018-12-17 22:40:29 +00:00
|
|
|
{
|
2022-05-07 15:12:15 +00:00
|
|
|
if (json_version == 1) {
|
|
|
|
return this->stream_dict.getJSON(json_version);
|
|
|
|
}
|
|
|
|
return getStreamJSON(json_version, qpdf_sj_none, qpdf_dl_none, nullptr, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
JSON
|
|
|
|
QPDF_Stream::getStreamJSON(
|
|
|
|
int json_version,
|
2022-05-07 17:33:45 +00:00
|
|
|
qpdf_json_stream_data_e json_data,
|
2022-05-07 15:12:15 +00:00
|
|
|
qpdf_stream_decode_level_e decode_level,
|
|
|
|
Pipeline* p,
|
|
|
|
std::string const& data_filename)
|
|
|
|
{
|
|
|
|
switch (json_data) {
|
|
|
|
case qpdf_sj_none:
|
|
|
|
case qpdf_sj_inline:
|
|
|
|
if (p != nullptr) {
|
|
|
|
throw std::logic_error("QPDF_Stream::getStreamJSON: pipline should "
|
|
|
|
"only be suppiled json_data is file");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case qpdf_sj_file:
|
|
|
|
if (p == nullptr) {
|
|
|
|
throw std::logic_error("QPDF_Stream::getStreamJSON: pipline must "
|
|
|
|
"be be suppiled json_data is file");
|
|
|
|
}
|
|
|
|
if (data_filename.empty()) {
|
|
|
|
throw std::logic_error("QPDF_Stream::getStreamJSON: data_filename "
|
|
|
|
"must be supplied when json_data is file");
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto dict = this->stream_dict;
|
|
|
|
JSON result = JSON::makeDictionary();
|
|
|
|
if (json_data != qpdf_sj_none) {
|
|
|
|
std::shared_ptr<Buffer> buf;
|
|
|
|
bool filtered = false;
|
|
|
|
bool filter = (decode_level != qpdf_dl_none);
|
|
|
|
for (int attempt = 1; attempt <= 2; ++attempt) {
|
|
|
|
Pl_Discard discard;
|
|
|
|
std::shared_ptr<Pl_Buffer> buf_pl;
|
|
|
|
Pipeline* data_pipeline = nullptr;
|
|
|
|
if (json_data == qpdf_sj_file) {
|
|
|
|
// We need to capture the data to write
|
|
|
|
buf_pl = std::make_shared<Pl_Buffer>("stream data");
|
|
|
|
data_pipeline = buf_pl.get();
|
|
|
|
} else {
|
|
|
|
data_pipeline = &discard;
|
|
|
|
}
|
2022-05-07 17:33:45 +00:00
|
|
|
bool succeeded = pipeStreamData(
|
|
|
|
data_pipeline,
|
|
|
|
&filtered,
|
|
|
|
0,
|
|
|
|
decode_level,
|
|
|
|
false,
|
|
|
|
(attempt == 1));
|
|
|
|
if ((!succeeded) || (filter && (!filtered))) {
|
2022-05-07 15:12:15 +00:00
|
|
|
// Try again
|
|
|
|
filter = false;
|
2022-05-07 17:33:45 +00:00
|
|
|
decode_level = qpdf_dl_none;
|
2022-05-07 15:12:15 +00:00
|
|
|
} else {
|
|
|
|
if (buf_pl.get()) {
|
|
|
|
buf = buf_pl->getBufferSharedPointer();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// We can use unsafeShallowCopy because we are only
|
|
|
|
// touching top-level keys.
|
|
|
|
dict = this->stream_dict.unsafeShallowCopy();
|
|
|
|
dict.removeKey("/Length");
|
2022-05-07 17:33:45 +00:00
|
|
|
if (filter && filtered) {
|
2022-05-07 15:12:15 +00:00
|
|
|
dict.removeKey("/Filter");
|
|
|
|
dict.removeKey("/DecodeParms");
|
|
|
|
}
|
|
|
|
if (json_data == qpdf_sj_file) {
|
|
|
|
result.addDictionaryMember(
|
|
|
|
"datafile", JSON::makeString(data_filename));
|
|
|
|
if (!buf.get()) {
|
|
|
|
throw std::logic_error(
|
|
|
|
"QPDF_Stream: failed to get stream data in json file mode");
|
|
|
|
}
|
|
|
|
p->write(buf->getBuffer(), buf->getSize());
|
|
|
|
} else if (json_data == qpdf_sj_inline) {
|
|
|
|
result.addDictionaryMember(
|
|
|
|
"data", JSON::makeBlob(StreamBlobProvider(this, decode_level)));
|
|
|
|
} else {
|
|
|
|
throw std::logic_error(
|
|
|
|
"QPDF_Stream: unexpected value of json_data");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
result.addDictionaryMember("dict", dict.getJSON(json_version));
|
|
|
|
return result;
|
2018-12-17 22:40:29 +00:00
|
|
|
}
|
|
|
|
|
2013-01-22 14:57:07 +00:00
|
|
|
QPDFObject::object_type_e
|
|
|
|
QPDF_Stream::getTypeCode() const
|
|
|
|
{
|
|
|
|
return QPDFObject::ot_stream;
|
|
|
|
}
|
|
|
|
|
|
|
|
char const*
|
|
|
|
QPDF_Stream::getTypeName() const
|
|
|
|
{
|
|
|
|
return "stream";
|
|
|
|
}
|
|
|
|
|
2018-02-16 22:25:27 +00:00
|
|
|
void
|
|
|
|
QPDF_Stream::setDescription(QPDF* qpdf, std::string const& description)
|
|
|
|
{
|
|
|
|
this->QPDFObject::setDescription(qpdf, description);
|
|
|
|
setDictDescription();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
QPDF_Stream::setDictDescription()
|
|
|
|
{
|
|
|
|
QPDF* qpdf = 0;
|
|
|
|
std::string description;
|
2022-04-02 21:14:10 +00:00
|
|
|
if ((!this->stream_dict.hasObjectDescription()) &&
|
|
|
|
getDescription(qpdf, description)) {
|
2018-02-16 22:25:27 +00:00
|
|
|
this->stream_dict.setObjectDescription(
|
|
|
|
qpdf, description + " -> stream dictionary");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-04-29 12:55:25 +00:00
|
|
|
QPDFObjectHandle
|
|
|
|
QPDF_Stream::getDict() const
|
|
|
|
{
|
|
|
|
return this->stream_dict;
|
|
|
|
}
|
|
|
|
|
2018-02-02 23:21:34 +00:00
|
|
|
bool
|
|
|
|
QPDF_Stream::isDataModified() const
|
|
|
|
{
|
2022-04-02 21:14:10 +00:00
|
|
|
return (!this->token_filters.empty());
|
2018-02-02 23:21:34 +00:00
|
|
|
}
|
|
|
|
|
2019-01-07 02:18:36 +00:00
|
|
|
qpdf_offset_t
|
|
|
|
QPDF_Stream::getOffset() const
|
|
|
|
{
|
|
|
|
return this->offset;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t
|
|
|
|
QPDF_Stream::getLength() const
|
|
|
|
{
|
|
|
|
return this->length;
|
|
|
|
}
|
|
|
|
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<Buffer>
|
2019-01-07 02:18:36 +00:00
|
|
|
QPDF_Stream::getStreamDataBuffer() const
|
|
|
|
{
|
|
|
|
return this->stream_data;
|
|
|
|
}
|
|
|
|
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<QPDFObjectHandle::StreamDataProvider>
|
2019-01-07 02:18:36 +00:00
|
|
|
QPDF_Stream::getStreamDataProvider() const
|
|
|
|
{
|
|
|
|
return this->stream_provider;
|
|
|
|
}
|
|
|
|
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<Buffer>
|
2017-08-19 13:18:14 +00:00
|
|
|
QPDF_Stream::getStreamData(qpdf_stream_decode_level_e decode_level)
|
2008-04-29 12:55:25 +00:00
|
|
|
{
|
|
|
|
Pl_Buffer buf("stream data buffer");
|
2020-04-08 22:47:29 +00:00
|
|
|
bool filtered;
|
|
|
|
pipeStreamData(&buf, &filtered, 0, decode_level, false, false);
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!filtered) {
|
|
|
|
throw QPDFExc(
|
|
|
|
qpdf_e_unsupported,
|
|
|
|
qpdf->getFilename(),
|
|
|
|
"",
|
|
|
|
this->offset,
|
|
|
|
"getStreamData called on unfilterable stream");
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
2010-08-09 23:33:40 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream getStreamData");
|
2022-02-06 16:40:24 +00:00
|
|
|
return buf.getBufferSharedPointer();
|
2010-08-09 23:33:40 +00:00
|
|
|
}
|
|
|
|
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<Buffer>
|
2010-08-09 23:33:40 +00:00
|
|
|
QPDF_Stream::getRawStreamData()
|
|
|
|
{
|
|
|
|
Pl_Buffer buf("stream data buffer");
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!pipeStreamData(&buf, nullptr, 0, qpdf_dl_none, false, false)) {
|
|
|
|
throw QPDFExc(
|
|
|
|
qpdf_e_unsupported,
|
|
|
|
qpdf->getFilename(),
|
|
|
|
"",
|
|
|
|
this->offset,
|
|
|
|
"error getting raw stream data");
|
2020-04-05 03:35:35 +00:00
|
|
|
}
|
2010-08-09 23:33:40 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream getRawStreamData");
|
2022-02-06 16:40:24 +00:00
|
|
|
return buf.getBufferSharedPointer();
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2012-12-30 00:00:05 +00:00
|
|
|
bool
|
2020-12-23 11:12:49 +00:00
|
|
|
QPDF_Stream::filterable(
|
|
|
|
std::vector<std::shared_ptr<QPDFStreamFilter>>& filters,
|
|
|
|
bool& specialized_compression,
|
|
|
|
bool& lossy_compression)
|
2008-04-29 12:55:25 +00:00
|
|
|
{
|
|
|
|
// Check filters
|
|
|
|
|
|
|
|
QPDFObjectHandle filter_obj = this->stream_dict.getKey("/Filter");
|
|
|
|
bool filters_okay = true;
|
|
|
|
|
2020-12-23 11:12:49 +00:00
|
|
|
std::vector<std::string> filter_names;
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filter_obj.isNull()) {
|
2022-02-08 14:18:08 +00:00
|
|
|
// No filters
|
2022-04-02 21:14:10 +00:00
|
|
|
} else if (filter_obj.isName()) {
|
2022-02-08 14:18:08 +00:00
|
|
|
// One filter
|
|
|
|
filter_names.push_back(filter_obj.getName());
|
2022-04-02 21:14:10 +00:00
|
|
|
} else if (filter_obj.isArray()) {
|
2022-02-08 14:18:08 +00:00
|
|
|
// Potentially multiple filters
|
|
|
|
int n = filter_obj.getArrayNItems();
|
2022-04-02 21:14:10 +00:00
|
|
|
for (int i = 0; i < n; ++i) {
|
2022-02-08 14:18:08 +00:00
|
|
|
QPDFObjectHandle item = filter_obj.getArrayItem(i);
|
2022-04-02 21:14:10 +00:00
|
|
|
if (item.isName()) {
|
2022-02-08 14:18:08 +00:00
|
|
|
filter_names.push_back(item.getName());
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
2022-02-08 14:18:08 +00:00
|
|
|
filters_okay = false;
|
|
|
|
}
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
2022-02-08 14:18:08 +00:00
|
|
|
filters_okay = false;
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!filters_okay) {
|
2022-02-08 14:18:08 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream invalid filter");
|
2022-04-23 22:03:44 +00:00
|
|
|
warn(
|
2022-04-02 21:14:10 +00:00
|
|
|
qpdf_e_damaged_pdf,
|
|
|
|
this->offset,
|
2022-04-23 22:03:44 +00:00
|
|
|
"stream filter type is not name or array");
|
2017-07-27 22:18:18 +00:00
|
|
|
return false;
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2012-12-30 00:00:05 +00:00
|
|
|
bool filterable = true;
|
2008-04-29 12:55:25 +00:00
|
|
|
|
2022-04-30 13:43:07 +00:00
|
|
|
for (auto& filter_name: filter_names) {
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filter_abbreviations.count(filter_name)) {
|
2022-02-08 14:18:08 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream expand filter abbreviation");
|
|
|
|
filter_name = filter_abbreviations[filter_name];
|
|
|
|
}
|
2010-09-05 15:00:44 +00:00
|
|
|
|
2020-12-23 11:12:49 +00:00
|
|
|
auto ff = filter_factories.find(filter_name);
|
2022-04-02 21:14:10 +00:00
|
|
|
if (ff == filter_factories.end()) {
|
2020-12-23 11:12:49 +00:00
|
|
|
filterable = false;
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
2020-12-23 11:12:49 +00:00
|
|
|
filters.push_back((ff->second)());
|
2017-08-19 13:18:14 +00:00
|
|
|
}
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!filterable) {
|
2012-12-30 00:00:05 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-12-23 11:12:49 +00:00
|
|
|
// filters now contains a list of filters to be applied in order.
|
|
|
|
// See which ones we can support.
|
2012-12-30 00:00:05 +00:00
|
|
|
|
|
|
|
// See if we can support any decode parameters that are specified.
|
|
|
|
|
|
|
|
QPDFObjectHandle decode_obj = this->stream_dict.getKey("/DecodeParms");
|
|
|
|
std::vector<QPDFObjectHandle> decode_parms;
|
2022-04-02 21:14:10 +00:00
|
|
|
if (decode_obj.isArray() && (decode_obj.getArrayNItems() == 0)) {
|
2019-06-09 21:19:07 +00:00
|
|
|
decode_obj = QPDFObjectHandle::newNull();
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
if (decode_obj.isArray()) {
|
|
|
|
for (int i = 0; i < decode_obj.getArrayNItems(); ++i) {
|
2012-12-30 00:00:05 +00:00
|
|
|
decode_parms.push_back(decode_obj.getArrayItem(i));
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
|
|
|
for (unsigned int i = 0; i < filter_names.size(); ++i) {
|
2012-12-30 00:00:05 +00:00
|
|
|
decode_parms.push_back(decode_obj);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-07-07 21:26:03 +00:00
|
|
|
// Ignore /DecodeParms entirely if /Filters is empty. At least
|
|
|
|
// one case of a file whose /DecodeParms was [ << >> ] when
|
|
|
|
// /Filters was empty has been seen in the wild.
|
2022-04-02 21:14:10 +00:00
|
|
|
if ((filters.size() != 0) && (decode_parms.size() != filters.size())) {
|
2022-04-23 22:03:44 +00:00
|
|
|
warn(
|
2022-04-02 21:14:10 +00:00
|
|
|
qpdf_e_damaged_pdf,
|
|
|
|
this->offset,
|
|
|
|
"stream /DecodeParms length is"
|
2022-04-23 22:03:44 +00:00
|
|
|
" inconsistent with filters");
|
2017-07-27 22:18:18 +00:00
|
|
|
filterable = false;
|
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!filterable) {
|
2017-07-27 22:18:18 +00:00
|
|
|
return false;
|
2012-12-30 00:00:05 +00:00
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
for (size_t i = 0; i < filters.size(); ++i) {
|
2020-12-23 11:12:49 +00:00
|
|
|
auto filter = filters.at(i);
|
|
|
|
auto decode_item = decode_parms.at(i);
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filter->setDecodeParms(decode_item)) {
|
|
|
|
if (filter->isSpecializedCompression()) {
|
2020-12-23 11:12:49 +00:00
|
|
|
specialized_compression = true;
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filter->isLossyCompression()) {
|
2020-12-23 11:12:49 +00:00
|
|
|
specialized_compression = true;
|
|
|
|
lossy_compression = true;
|
2012-12-30 00:00:05 +00:00
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
2012-12-30 00:00:05 +00:00
|
|
|
filterable = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2008-04-29 12:55:25 +00:00
|
|
|
return filterable;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
2022-04-02 21:14:10 +00:00
|
|
|
QPDF_Stream::pipeStreamData(
|
|
|
|
Pipeline* pipeline,
|
|
|
|
bool* filterp,
|
|
|
|
int encode_flags,
|
|
|
|
qpdf_stream_decode_level_e decode_level,
|
|
|
|
bool suppress_warnings,
|
|
|
|
bool will_retry)
|
2008-04-29 12:55:25 +00:00
|
|
|
{
|
2020-12-23 11:12:49 +00:00
|
|
|
std::vector<std::shared_ptr<QPDFStreamFilter>> filters;
|
2017-08-19 13:18:14 +00:00
|
|
|
bool specialized_compression = false;
|
|
|
|
bool lossy_compression = false;
|
2020-04-05 03:35:35 +00:00
|
|
|
bool ignored;
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filterp == nullptr) {
|
2020-04-05 03:35:35 +00:00
|
|
|
filterp = &ignored;
|
|
|
|
}
|
|
|
|
bool& filter = *filterp;
|
2022-04-02 21:14:10 +00:00
|
|
|
filter = (!((encode_flags == 0) && (decode_level == qpdf_dl_none)));
|
2020-04-05 03:35:35 +00:00
|
|
|
bool success = true;
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filter) {
|
|
|
|
filter =
|
|
|
|
filterable(filters, specialized_compression, lossy_compression);
|
|
|
|
if ((decode_level < qpdf_dl_all) && lossy_compression) {
|
2017-08-19 13:18:14 +00:00
|
|
|
filter = false;
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
if ((decode_level < qpdf_dl_specialized) && specialized_compression) {
|
2017-08-19 13:18:14 +00:00
|
|
|
filter = false;
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
QTC::TC(
|
|
|
|
"qpdf",
|
|
|
|
"QPDF_Stream special filters",
|
|
|
|
(!filter) ? 0
|
|
|
|
: lossy_compression ? 1
|
|
|
|
: specialized_compression ? 2
|
|
|
|
: 3);
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (pipeline == 0) {
|
2022-02-08 14:18:08 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream pipeStreamData with null pipeline");
|
2020-04-05 03:35:35 +00:00
|
|
|
// Return value is whether we can filter in this case.
|
2022-02-08 14:18:08 +00:00
|
|
|
return filter;
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2020-12-23 11:12:49 +00:00
|
|
|
// Construct the pipeline in reverse order. Force pipelines we
|
|
|
|
// create to be deleted when this function finishes. Pipelines
|
|
|
|
// created by QPDFStreamFilter objects will be deleted by those
|
|
|
|
// objects.
|
2022-02-04 16:03:52 +00:00
|
|
|
std::vector<std::shared_ptr<Pipeline>> to_delete;
|
2008-04-29 12:55:25 +00:00
|
|
|
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<ContentNormalizer> normalizer;
|
2022-02-04 16:03:52 +00:00
|
|
|
std::shared_ptr<Pipeline> new_pipeline;
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filter) {
|
|
|
|
if (encode_flags & qpdf_ef_compress) {
|
2022-02-08 14:18:08 +00:00
|
|
|
new_pipeline = std::make_shared<Pl_Flate>(
|
2022-02-04 16:03:52 +00:00
|
|
|
"compress stream", pipeline, Pl_Flate::a_deflate);
|
2022-02-08 14:18:08 +00:00
|
|
|
to_delete.push_back(new_pipeline);
|
2022-02-04 16:03:52 +00:00
|
|
|
pipeline = new_pipeline.get();
|
2022-02-08 14:18:08 +00:00
|
|
|
}
|
2008-04-29 12:55:25 +00:00
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (encode_flags & qpdf_ef_normalize) {
|
2022-04-09 18:35:56 +00:00
|
|
|
normalizer = std::make_shared<ContentNormalizer>();
|
2022-02-08 14:18:08 +00:00
|
|
|
new_pipeline = std::make_shared<Pl_QPDFTokenizer>(
|
2022-02-04 15:10:19 +00:00
|
|
|
"normalizer", normalizer.get(), pipeline);
|
2022-02-08 14:18:08 +00:00
|
|
|
to_delete.push_back(new_pipeline);
|
2022-02-04 16:03:52 +00:00
|
|
|
pipeline = new_pipeline.get();
|
2022-02-08 14:18:08 +00:00
|
|
|
}
|
2008-04-29 12:55:25 +00:00
|
|
|
|
2022-02-04 16:03:52 +00:00
|
|
|
for (auto iter = this->token_filters.rbegin();
|
2022-04-02 21:14:10 +00:00
|
|
|
iter != this->token_filters.rend();
|
|
|
|
++iter) {
|
2022-02-04 16:03:52 +00:00
|
|
|
new_pipeline = std::make_shared<Pl_QPDFTokenizer>(
|
2022-02-04 15:10:19 +00:00
|
|
|
"token filter", (*iter).get(), pipeline);
|
2022-02-04 16:03:52 +00:00
|
|
|
to_delete.push_back(new_pipeline);
|
|
|
|
pipeline = new_pipeline.get();
|
2018-02-02 23:21:34 +00:00
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
for (auto f_iter = filters.rbegin(); f_iter != filters.rend();
|
|
|
|
++f_iter) {
|
2020-12-23 11:12:49 +00:00
|
|
|
auto decode_pipeline = (*f_iter)->getDecodePipeline(pipeline);
|
2022-04-02 21:14:10 +00:00
|
|
|
if (decode_pipeline) {
|
2020-12-23 11:12:49 +00:00
|
|
|
pipeline = decode_pipeline;
|
2017-12-25 00:18:52 +00:00
|
|
|
}
|
2021-11-02 21:54:10 +00:00
|
|
|
Pl_Flate* flate = dynamic_cast<Pl_Flate*>(pipeline);
|
2022-04-02 21:14:10 +00:00
|
|
|
if (flate != nullptr) {
|
2021-11-02 21:54:10 +00:00
|
|
|
flate->setWarnCallback([this](char const* msg, int code) {
|
2022-04-23 22:03:44 +00:00
|
|
|
warn(qpdf_e_damaged_pdf, this->offset, msg);
|
2021-11-02 21:54:10 +00:00
|
|
|
});
|
|
|
|
}
|
2022-02-08 14:18:08 +00:00
|
|
|
}
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (this->stream_data.get()) {
|
2022-02-08 14:18:08 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream pipe replaced stream data");
|
2022-04-02 21:14:10 +00:00
|
|
|
pipeline->write(
|
|
|
|
this->stream_data->getBuffer(), this->stream_data->getSize());
|
2022-02-08 14:18:08 +00:00
|
|
|
pipeline->finish();
|
2022-04-02 21:14:10 +00:00
|
|
|
} else if (this->stream_provider.get()) {
|
2022-02-08 14:18:08 +00:00
|
|
|
Pl_Count count("stream provider count", pipeline);
|
2022-04-02 21:14:10 +00:00
|
|
|
if (this->stream_provider->supportsRetry()) {
|
|
|
|
if (!this->stream_provider->provideStreamData(
|
|
|
|
this->objid,
|
|
|
|
this->generation,
|
|
|
|
&count,
|
|
|
|
suppress_warnings,
|
|
|
|
will_retry)) {
|
2020-04-05 03:35:35 +00:00
|
|
|
filter = false;
|
|
|
|
success = false;
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
2020-04-05 03:35:35 +00:00
|
|
|
this->stream_provider->provideStreamData(
|
|
|
|
this->objid, this->generation, &count);
|
|
|
|
}
|
2022-02-08 14:18:08 +00:00
|
|
|
qpdf_offset_t actual_length = count.getCount();
|
|
|
|
qpdf_offset_t desired_length = 0;
|
2022-04-02 21:14:10 +00:00
|
|
|
if (success && this->stream_dict.hasKey("/Length")) {
|
2022-02-08 14:18:08 +00:00
|
|
|
desired_length = this->stream_dict.getKey("/Length").getIntValue();
|
2022-04-02 21:14:10 +00:00
|
|
|
if (actual_length == desired_length) {
|
2012-07-07 21:33:45 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream pipe use stream provider");
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
2012-07-07 21:33:45 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream provider length mismatch");
|
2017-07-27 22:18:18 +00:00
|
|
|
// This would be caused by programmer error on the
|
|
|
|
// part of a library user, not by invalid input data.
|
|
|
|
throw std::runtime_error(
|
2012-07-07 21:33:45 +00:00
|
|
|
"stream data provider for " +
|
|
|
|
QUtil::int_to_string(this->objid) + " " +
|
2022-04-02 21:14:10 +00:00
|
|
|
QUtil::int_to_string(this->generation) + " provided " +
|
2012-07-07 21:33:45 +00:00
|
|
|
QUtil::int_to_string(actual_length) +
|
|
|
|
" bytes instead of expected " +
|
|
|
|
QUtil::int_to_string(desired_length) + " bytes");
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
} else if (success) {
|
2012-07-07 21:33:45 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream provider length not provided");
|
|
|
|
this->stream_dict.replaceKey(
|
|
|
|
"/Length", QPDFObjectHandle::newInteger(actual_length));
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
} else if (this->offset == 0) {
|
2022-02-08 14:18:08 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream pipe no stream data");
|
2022-04-02 21:14:10 +00:00
|
|
|
throw std::logic_error("pipeStreamData called for stream with no data");
|
|
|
|
} else {
|
2022-02-08 14:18:08 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream pipe original stream data");
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!QPDF::Pipe::pipeStreamData(
|
|
|
|
this->qpdf,
|
|
|
|
this->objid,
|
|
|
|
this->generation,
|
|
|
|
this->offset,
|
|
|
|
this->length,
|
|
|
|
this->stream_dict,
|
|
|
|
pipeline,
|
|
|
|
suppress_warnings,
|
|
|
|
will_retry)) {
|
2017-07-28 03:42:27 +00:00
|
|
|
filter = false;
|
2020-04-05 03:35:35 +00:00
|
|
|
success = false;
|
2017-07-28 03:42:27 +00:00
|
|
|
}
|
2010-08-02 22:40:52 +00:00
|
|
|
}
|
2008-04-29 12:55:25 +00:00
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (filter && (!suppress_warnings) && normalizer.get() &&
|
|
|
|
normalizer->anyBadTokens()) {
|
2022-04-23 22:03:44 +00:00
|
|
|
warn(
|
2022-04-02 21:14:10 +00:00
|
|
|
qpdf_e_damaged_pdf,
|
|
|
|
this->offset,
|
2022-04-23 22:03:44 +00:00
|
|
|
"content normalization encountered bad tokens");
|
2022-04-02 21:14:10 +00:00
|
|
|
if (normalizer->lastTokenWasBad()) {
|
2018-02-03 02:16:40 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream bad token at end during normalize");
|
2022-04-23 22:03:44 +00:00
|
|
|
warn(
|
2022-04-02 21:14:10 +00:00
|
|
|
qpdf_e_damaged_pdf,
|
|
|
|
this->offset,
|
|
|
|
"normalized content ended with a bad token;"
|
|
|
|
" you may be able to resolve this by"
|
|
|
|
" coalescing content streams in combination"
|
|
|
|
" with normalizing content. From the command"
|
2022-04-23 22:03:44 +00:00
|
|
|
" line, specify --coalesce-contents");
|
2018-02-03 02:16:40 +00:00
|
|
|
}
|
2022-04-23 22:03:44 +00:00
|
|
|
warn(
|
2022-04-02 21:14:10 +00:00
|
|
|
qpdf_e_damaged_pdf,
|
|
|
|
this->offset,
|
|
|
|
"Resulting stream data may be corrupted but is"
|
|
|
|
" may still useful for manual inspection."
|
|
|
|
" For more information on this warning, search"
|
2022-04-23 22:03:44 +00:00
|
|
|
" for content normalization in the manual.");
|
2018-02-03 02:16:40 +00:00
|
|
|
}
|
|
|
|
|
2020-04-05 03:35:35 +00:00
|
|
|
return success;
|
2008-04-29 12:55:25 +00:00
|
|
|
}
|
2010-08-02 22:17:01 +00:00
|
|
|
|
|
|
|
void
|
2022-04-02 21:14:10 +00:00
|
|
|
QPDF_Stream::replaceStreamData(
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<Buffer> data,
|
2022-04-02 21:14:10 +00:00
|
|
|
QPDFObjectHandle const& filter,
|
|
|
|
QPDFObjectHandle const& decode_parms)
|
2010-08-02 22:17:01 +00:00
|
|
|
{
|
|
|
|
this->stream_data = data;
|
2010-08-05 19:04:22 +00:00
|
|
|
this->stream_provider = 0;
|
2010-09-24 20:45:18 +00:00
|
|
|
replaceFilterData(filter, decode_parms, data->getSize());
|
2010-08-05 19:04:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
QPDF_Stream::replaceStreamData(
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<QPDFObjectHandle::StreamDataProvider> provider,
|
2010-08-05 19:04:22 +00:00
|
|
|
QPDFObjectHandle const& filter,
|
2012-07-07 21:33:45 +00:00
|
|
|
QPDFObjectHandle const& decode_parms)
|
2010-08-05 19:04:22 +00:00
|
|
|
{
|
|
|
|
this->stream_provider = provider;
|
|
|
|
this->stream_data = 0;
|
2012-07-07 21:33:45 +00:00
|
|
|
replaceFilterData(filter, decode_parms, 0);
|
2010-08-05 19:04:22 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 23:21:34 +00:00
|
|
|
void
|
|
|
|
QPDF_Stream::addTokenFilter(
|
2022-04-09 18:35:56 +00:00
|
|
|
std::shared_ptr<QPDFObjectHandle::TokenFilter> token_filter)
|
2018-02-02 23:21:34 +00:00
|
|
|
{
|
|
|
|
this->token_filters.push_back(token_filter);
|
|
|
|
}
|
|
|
|
|
2010-08-05 19:04:22 +00:00
|
|
|
void
|
2022-04-02 21:14:10 +00:00
|
|
|
QPDF_Stream::replaceFilterData(
|
|
|
|
QPDFObjectHandle const& filter,
|
|
|
|
QPDFObjectHandle const& decode_parms,
|
|
|
|
size_t length)
|
2010-08-05 19:04:22 +00:00
|
|
|
{
|
2022-05-17 22:35:35 +00:00
|
|
|
if (filter.isInitialized()) {
|
|
|
|
this->stream_dict.replaceKey("/Filter", filter);
|
|
|
|
}
|
|
|
|
if (decode_parms.isInitialized()) {
|
|
|
|
this->stream_dict.replaceKey("/DecodeParms", decode_parms);
|
|
|
|
}
|
2022-04-02 21:14:10 +00:00
|
|
|
if (length == 0) {
|
2012-07-07 21:33:45 +00:00
|
|
|
QTC::TC("qpdf", "QPDF_Stream unknown stream length");
|
|
|
|
this->stream_dict.removeKey("/Length");
|
2022-04-02 21:14:10 +00:00
|
|
|
} else {
|
2012-07-07 21:33:45 +00:00
|
|
|
this->stream_dict.replaceKey(
|
2022-04-02 21:14:10 +00:00
|
|
|
"/Length",
|
|
|
|
QPDFObjectHandle::newInteger(QIntC::to_longlong(length)));
|
2012-07-07 21:33:45 +00:00
|
|
|
}
|
2010-08-02 22:17:01 +00:00
|
|
|
}
|
2012-07-21 13:00:06 +00:00
|
|
|
|
|
|
|
void
|
2022-04-24 13:05:50 +00:00
|
|
|
QPDF_Stream::replaceDict(QPDFObjectHandle const& new_dict)
|
2012-07-21 13:00:06 +00:00
|
|
|
{
|
|
|
|
this->stream_dict = new_dict;
|
2018-02-16 22:25:27 +00:00
|
|
|
setDictDescription();
|
2012-07-21 13:00:06 +00:00
|
|
|
}
|
2017-07-27 22:18:18 +00:00
|
|
|
|
|
|
|
void
|
2022-04-23 22:03:44 +00:00
|
|
|
QPDF_Stream::warn(
|
|
|
|
qpdf_error_code_e error_code,
|
|
|
|
qpdf_offset_t offset,
|
|
|
|
std::string const& message)
|
2017-07-27 22:18:18 +00:00
|
|
|
{
|
2022-04-23 22:03:44 +00:00
|
|
|
this->qpdf->warn(error_code, "", offset, message);
|
2017-07-27 22:18:18 +00:00
|
|
|
}
|