#include #include #include #include #include #include #include #include #include #include static char const* whoami = 0; void usage() { std::cerr << "Usage: " << whoami << " infile.pdf outfile.pdf [in-password]" << std::endl << "Invert some images in infile.pdf;" << " write output to outfile.pdf" << std::endl; exit(2); } // 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. class ImageInverter: public QPDFObjectHandle::StreamDataProvider { public: virtual ~ImageInverter() = default; virtual void provideStreamData(int objid, int generation, Pipeline* pipeline) override; void registerImage( QPDFObjectHandle image, std::shared_ptr self); private: std::map copied_images; }; void ImageInverter::registerImage( QPDFObjectHandle image, std::shared_ptr self) { // 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). QPDFObjGen og(image.getObjGen()); // 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. if (this->copied_images.count(og) > 0) { return; } this->copied_images[og] = image.copyStream(); // 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. image.replaceStreamData( self, QPDFObjectHandle::newNull(), QPDFObjectHandle::newNull()); } void ImageInverter::provideStreamData(int objid, int generation, Pipeline* pipeline) { // 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. QPDFObjGen og(objid, generation); std::shared_ptr data = this->copied_images[og].getStreamData(qpdf_dl_all); size_t size = data->getSize(); unsigned char* buf = data->getBuffer(); unsigned char ch; for (size_t i = 0; i < size; ++i) { ch = QIntC::to_uchar(0xff - buf[i]); pipeline->write(&ch, 1); } pipeline->finish(); } int main(int argc, char* argv[]) { whoami = QUtil::getWhoami(argv[0]); // For test suite bool static_id = false; if ((argc > 1) && (strcmp(argv[1], " --static-id") == 0)) { static_id = true; --argc; ++argv; } if (!((argc == 3) || (argc == 4))) { usage(); } char const* infilename = argv[1]; char const* outfilename = argv[2]; char const* password = (argc == 4) ? argv[3] : ""; try { QPDF qpdf; qpdf.processFile(infilename, password); ImageInverter* inv = new ImageInverter; auto p = std::shared_ptr(inv); // For each page... std::vector pages = QPDFPageDocumentHelper(qpdf).getAllPages(); for (std::vector::iterator iter = pages.begin(); iter != pages.end(); ++iter) { QPDFPageObjectHelper& page(*iter); // Get all images on the page. std::map images = page.getImages(); for (auto& iter2: images) { QPDFObjectHandle& image = iter2.second; QPDFObjectHandle image_dict = image.getDict(); QPDFObjectHandle color_space = image_dict.getKey("/ColorSpace"); QPDFObjectHandle bits_per_component = image_dict.getKey("/BitsPerComponent"); // For our example, we can only work with images 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. if (image.pipeStreamData(0, qpdf_ef_compress, qpdf_dl_all) && color_space.isNameAndEquals("/DeviceGray") && bits_per_component.isInteger() && (bits_per_component.getIntValue() == 8)) { inv->registerImage(image, p); } } } // Write out a new file QPDFWriter w(qpdf, outfilename); if (static_id) { // For the test suite, uncompress streams and use static // IDs. w.setStaticID(true); // for testing only } w.write(); std::cout << whoami << ": new file written to " << outfilename << std::endl; } catch (std::exception& e) { std::cerr << whoami << " processing file " << infilename << ": " << e.what() << std::endl; exit(2); } return 0; }