2022-04-02 21:14:10 +00:00
|
|
|
#include <qpdf/Buffer.hh>
|
|
|
|
#include <qpdf/QIntC.hh>
|
2010-08-06 01:27:47 +00:00
|
|
|
#include <qpdf/QPDF.hh>
|
2018-06-18 19:06:51 +00:00
|
|
|
#include <qpdf/QPDFPageDocumentHelper.hh>
|
|
|
|
#include <qpdf/QPDFPageObjectHelper.hh>
|
2010-08-06 01:27:47 +00:00
|
|
|
#include <qpdf/QPDFWriter.hh>
|
2022-04-02 21:14:10 +00:00
|
|
|
#include <qpdf/QUtil.hh>
|
2023-05-20 11:22:32 +00:00
|
|
|
#include <cstdlib>
|
|
|
|
#include <cstring>
|
2023-05-20 18:13:09 +00:00
|
|
|
#include <iostream>
|
2010-08-06 01:27:47 +00:00
|
|
|
|
2022-07-26 11:37:50 +00:00
|
|
|
static char const* whoami = nullptr;
|
2010-08-06 01:27:47 +00:00
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
void
|
|
|
|
usage()
|
2010-08-06 01:27:47 +00:00
|
|
|
{
|
2023-05-21 17:35:09 +00:00
|
|
|
std::cerr << "Usage: " << whoami << " infile.pdf outfile.pdf [in-password]" << std::endl
|
2023-12-23 02:45:10 +00:00
|
|
|
<< "Invert some images in infile.pdf; write output to outfile.pdf" << std::endl;
|
2010-08-06 01:27:47 +00:00
|
|
|
exit(2);
|
|
|
|
}
|
|
|
|
|
2023-05-27 17:19:52 +00:00
|
|
|
// Derive a class from StreamDataProvider to provide updated stream data. The main purpose of using
|
|
|
|
// this object is to avoid having to allocate memory up front for the objects. We want to replace
|
|
|
|
// the stream data with a function of the original stream data. In order to do this without actually
|
|
|
|
// holding all the images in memory, we create copies of the streams. Copying the streams doesn't
|
|
|
|
// actually copy the data. Internally, the qpdf library is holding onto the location of the original
|
|
|
|
// stream data, which makes it possible for the StreamDataProvider to access it when it needs it.
|
2010-08-06 01:27:47 +00:00
|
|
|
class ImageInverter: public QPDFObjectHandle::StreamDataProvider
|
|
|
|
{
|
|
|
|
public:
|
2023-05-20 13:25:46 +00:00
|
|
|
~ImageInverter() override = default;
|
2023-05-20 18:13:09 +00:00
|
|
|
void provideStreamData(QPDFObjGen const& og, Pipeline* pipeline) override;
|
2010-08-06 01:27:47 +00:00
|
|
|
|
2020-04-08 23:46:10 +00:00
|
|
|
void registerImage(
|
2023-05-21 17:35:09 +00:00
|
|
|
QPDFObjectHandle image, std::shared_ptr<QPDFObjectHandle::StreamDataProvider> self);
|
2020-04-07 22:11:00 +00:00
|
|
|
|
|
|
|
private:
|
|
|
|
std::map<QPDFObjGen, QPDFObjectHandle> copied_images;
|
2010-08-06 01:27:47 +00:00
|
|
|
};
|
|
|
|
|
2020-04-07 22:11:00 +00:00
|
|
|
void
|
2020-04-08 23:46:10 +00:00
|
|
|
ImageInverter::registerImage(
|
2023-05-21 17:35:09 +00:00
|
|
|
QPDFObjectHandle image, std::shared_ptr<QPDFObjectHandle::StreamDataProvider> self)
|
2020-04-07 22:11:00 +00:00
|
|
|
{
|
2023-05-27 17:19:52 +00:00
|
|
|
// replaceStreamData requires a pointer holder to the stream data provider, but there's no way
|
|
|
|
// for us to generate one ourselves, so we have to have it handed to us. Don't be tempted to
|
|
|
|
// have the class contain a std::shared_ptr to itself as a member. Doing this will prevent the
|
|
|
|
// class from ever being deleted since the reference count will never drop to zero (and
|
|
|
|
// std::shared_ptr doesn't have weak references).
|
2020-04-07 22:11:00 +00:00
|
|
|
|
|
|
|
QPDFObjGen og(image.getObjGen());
|
2023-05-27 17:19:52 +00:00
|
|
|
// Store information about the images based on the object and generation number. Recall that a
|
|
|
|
// single image object may be used more than once, so no need to update the same stream multiple
|
|
|
|
// times.
|
2022-04-02 21:14:10 +00:00
|
|
|
if (this->copied_images.count(og) > 0) {
|
2020-04-07 22:11:00 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-02-25 17:32:45 +00:00
|
|
|
this->copied_images[og] = image.copyStream();
|
2020-04-07 22:11:00 +00:00
|
|
|
|
2023-05-27 17:19:52 +00:00
|
|
|
// Register our stream data provider for this stream. Future calls to getStreamData or
|
|
|
|
// pipeStreamData will use the new information. Provide null for both filter and decode
|
|
|
|
// parameters. Note that this does not mean the image data will be uncompressed when we write
|
|
|
|
// the file. By default, QPDFWriter will use /FlateDecode for anything that is uncompressed or
|
|
|
|
// filterable in the input QPDF object, so we don't have to deal with it explicitly here. We
|
|
|
|
// could explicitly use /DCTDecode and write through a DCT filter if we wanted.
|
2023-05-21 17:35:09 +00:00
|
|
|
image.replaceStreamData(self, QPDFObjectHandle::newNull(), QPDFObjectHandle::newNull());
|
2020-04-07 22:11:00 +00:00
|
|
|
}
|
|
|
|
|
2010-08-06 01:27:47 +00:00
|
|
|
void
|
2022-07-24 13:16:37 +00:00
|
|
|
ImageInverter::provideStreamData(QPDFObjGen const& og, Pipeline* pipeline)
|
2010-08-06 01:27:47 +00:00
|
|
|
{
|
2023-05-27 17:19:52 +00:00
|
|
|
// Use the object and generation number supplied to look up the image data. Then invert the
|
|
|
|
// image data and write the inverted data to the pipeline.
|
2023-05-21 17:35:09 +00:00
|
|
|
std::shared_ptr<Buffer> data = this->copied_images[og].getStreamData(qpdf_dl_all);
|
2010-09-24 20:45:18 +00:00
|
|
|
size_t size = data->getSize();
|
|
|
|
unsigned char* buf = data->getBuffer();
|
2010-08-06 01:27:47 +00:00
|
|
|
unsigned char ch;
|
2022-04-02 21:14:10 +00:00
|
|
|
for (size_t i = 0; i < size; ++i) {
|
2022-02-08 14:18:08 +00:00
|
|
|
ch = QIntC::to_uchar(0xff - buf[i]);
|
|
|
|
pipeline->write(&ch, 1);
|
2010-08-06 01:27:47 +00:00
|
|
|
}
|
|
|
|
pipeline->finish();
|
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
int
|
|
|
|
main(int argc, char* argv[])
|
2010-08-06 01:27:47 +00:00
|
|
|
{
|
|
|
|
whoami = QUtil::getWhoami(argv[0]);
|
|
|
|
|
2013-11-30 03:08:25 +00:00
|
|
|
// For test suite
|
|
|
|
bool static_id = false;
|
2022-04-02 21:14:10 +00:00
|
|
|
if ((argc > 1) && (strcmp(argv[1], " --static-id") == 0)) {
|
2013-11-30 03:08:25 +00:00
|
|
|
static_id = true;
|
|
|
|
--argc;
|
|
|
|
++argv;
|
|
|
|
}
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
if (!((argc == 3) || (argc == 4))) {
|
2022-02-08 14:18:08 +00:00
|
|
|
usage();
|
2010-08-06 01:27:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
char const* infilename = argv[1];
|
|
|
|
char const* outfilename = argv[2];
|
|
|
|
char const* password = (argc == 4) ? argv[3] : "";
|
|
|
|
|
2022-04-02 21:14:10 +00:00
|
|
|
try {
|
2022-02-08 14:18:08 +00:00
|
|
|
QPDF qpdf;
|
|
|
|
qpdf.processFile(infilename, password);
|
2010-08-06 01:27:47 +00:00
|
|
|
|
2023-05-20 12:24:10 +00:00
|
|
|
auto* inv = new ImageInverter;
|
2022-04-09 18:49:10 +00:00
|
|
|
auto p = std::shared_ptr<QPDFObjectHandle::StreamDataProvider>(inv);
|
2010-08-06 01:27:47 +00:00
|
|
|
|
2022-02-08 14:18:08 +00:00
|
|
|
// For each page...
|
2022-05-21 14:18:15 +00:00
|
|
|
for (auto& page: QPDFPageDocumentHelper(qpdf).getAllPages()) {
|
2022-02-08 14:18:08 +00:00
|
|
|
// Get all images on the page.
|
2022-05-21 14:18:15 +00:00
|
|
|
for (auto& iter: page.getImages()) {
|
2022-04-30 17:23:18 +00:00
|
|
|
QPDFObjectHandle& image = iter.second;
|
2022-02-08 14:18:08 +00:00
|
|
|
QPDFObjectHandle image_dict = image.getDict();
|
2022-04-02 21:14:10 +00:00
|
|
|
QPDFObjectHandle color_space = image_dict.getKey("/ColorSpace");
|
2023-05-21 17:35:09 +00:00
|
|
|
QPDFObjectHandle bits_per_component = image_dict.getKey("/BitsPerComponent");
|
2022-02-08 14:18:08 +00:00
|
|
|
|
2023-05-29 18:26:18 +00:00
|
|
|
// For our example, we can only work with 8-bit grayscale images that we can fully
|
|
|
|
// decode. Use pipeStreamData with a null pipeline to determine whether the image
|
|
|
|
// is filterable. Directly inspect keys to determine the image type.
|
2023-05-21 17:35:09 +00:00
|
|
|
if (image.pipeStreamData(nullptr, qpdf_ef_compress, qpdf_dl_all) &&
|
|
|
|
color_space.isNameAndEquals("/DeviceGray") && bits_per_component.isInteger() &&
|
2022-04-02 21:14:10 +00:00
|
|
|
(bits_per_component.getIntValue() == 8)) {
|
2020-04-08 23:46:10 +00:00
|
|
|
inv->registerImage(image, p);
|
2020-04-07 22:11:00 +00:00
|
|
|
}
|
2022-02-08 14:18:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write out a new file
|
|
|
|
QPDFWriter w(qpdf, outfilename);
|
2022-04-02 21:14:10 +00:00
|
|
|
if (static_id) {
|
2023-05-27 17:19:52 +00:00
|
|
|
// For the test suite, uncompress streams and use static IDs.
|
2022-02-08 14:18:08 +00:00
|
|
|
w.setStaticID(true); // for testing only
|
|
|
|
}
|
|
|
|
w.write();
|
2023-05-21 17:35:09 +00:00
|
|
|
std::cout << whoami << ": new file written to " << outfilename << std::endl;
|
2022-04-02 21:14:10 +00:00
|
|
|
} catch (std::exception& e) {
|
2023-05-21 17:35:09 +00:00
|
|
|
std::cerr << whoami << " processing file " << infilename << ": " << e.what() << std::endl;
|
2022-02-08 14:18:08 +00:00
|
|
|
exit(2);
|
2010-08-06 01:27:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|