// This program tests miscellaneous functionality in the qpdf library // that we don't want to pollute the qpdf program with. #include #include #include #include #include #include #include #include #include #include #include #include #include static char const* whoami = 0; void usage() { std::cerr << "Usage: " << whoami << " n filename" << std::endl; exit(2); } class Provider: public QPDFObjectHandle::StreamDataProvider { public: Provider(PointerHolder b) : b(b), bad_length(false) { } virtual ~Provider() { } virtual void provideStreamData(int objid, int generation, Pipeline* p) { p->write(b->getBuffer(), b->getSize()); if (this->bad_length) { unsigned char ch = ' '; p->write(&ch, 1); } p->finish(); } void badLength(bool v) { this->bad_length = v; } private: PointerHolder b; bool bad_length; }; void runtest(int n, char const* filename) { QPDF pdf; PointerHolder file_buf; if (n == 0) { pdf.setAttemptRecovery(false); } if (n % 2 == 0) { pdf.processFile(filename); } else { // Exercise processMemoryFile FILE* f = QUtil::fopen_wrapper(std::string("open ") + filename, fopen(filename, "rb")); fseek(f, 0, SEEK_END); size_t size = (size_t) QUtil::ftell_off_t(f); fseek(f, 0, SEEK_SET); file_buf = PointerHolder(true, new char[size]); char* buf_p = file_buf.getPointer(); size_t bytes_read = 0; size_t len = 0; while ((len = fread(buf_p + bytes_read, 1, size - bytes_read, f)) > 0) { bytes_read += len; } if (bytes_read != size) { if (ferror(f)) { throw std::runtime_error( std::string("failure reading file ") + filename + " into memory: read " + QUtil::int_to_string(bytes_read) + "; wanted " + QUtil::int_to_string(size)); } else { throw std::logic_error( std::string("premature eof reading file ") + filename + " into memory: read " + QUtil::int_to_string(bytes_read) + "; wanted " + QUtil::int_to_string(size)); } } fclose(f); pdf.processMemoryFile(filename, buf_p, size); } if ((n == 0) || (n == 1)) { QPDFObjectHandle trailer = pdf.getTrailer(); QPDFObjectHandle qtest = trailer.getKey("/QTest"); if (! trailer.hasKey("/QTest")) { // This will always happen when /QTest is null because // hasKey returns false for null keys regardless of // whether the key exists or not. That way there's never // any difference between a key that is present and null // and a key that is absent. QTC::TC("qpdf", "main QTest implicit"); std::cout << "/QTest is implicit" << std::endl; } QTC::TC("qpdf", "main QTest indirect", qtest.isIndirect() ? 1 : 0); std::cout << "/QTest is " << (qtest.isIndirect() ? "in" : "") << "direct" << std::endl; if (qtest.isNull()) { QTC::TC("qpdf", "main QTest null"); std::cout << "/QTest is null" << std::endl; } else if (qtest.isBool()) { QTC::TC("qpdf", "main QTest bool", qtest.getBoolValue() ? 1 : 0); std::cout << "/QTest is Boolean with value " << (qtest.getBoolValue() ? "true" : "false") << std::endl; } else if (qtest.isInteger()) { QTC::TC("qpdf", "main QTest int"); std::cout << "/QTest is an integer with value " << qtest.getIntValue() << std::endl; } else if (qtest.isReal()) { QTC::TC("qpdf", "main QTest real"); std::cout << "/QTest is a real number with value " << qtest.getRealValue() << std::endl; } else if (qtest.isName()) { QTC::TC("qpdf", "main QTest name"); std::cout << "/QTest is a name with value " << qtest.getName() << std::endl; } else if (qtest.isString()) { QTC::TC("qpdf", "main QTest string"); std::cout << "/QTest is a string with value " << qtest.getStringValue() << std::endl; } else if (qtest.isArray()) { QTC::TC("qpdf", "main QTest array"); int n = qtest.getArrayNItems(); std::cout << "/QTest is an array with " << n << " items" << std::endl; for (int i = 0; i < n; ++i) { QTC::TC("qpdf", "main QTest array indirect", qtest.getArrayItem(i).isIndirect() ? 1 : 0); std::cout << " item " << i << " is " << (qtest.getArrayItem(i).isIndirect() ? "in" : "") << "direct" << std::endl; } } else if (qtest.isDictionary()) { QTC::TC("qpdf", "main QTest dictionary"); std::cout << "/QTest is a dictionary" << std::endl; std::set keys = qtest.getKeys(); for (std::set::iterator iter = keys.begin(); iter != keys.end(); ++iter) { QTC::TC("qpdf", "main QTest dictionary indirect", (qtest.getKey(*iter).isIndirect() ? 1 : 0)); std::cout << " " << *iter << " is " << (qtest.getKey(*iter).isIndirect() ? "in" : "") << "direct" << std::endl; } } else if (qtest.isStream()) { QTC::TC("qpdf", "main QTest stream"); std::cout << "/QTest is a stream. Dictionary: " << qtest.getDict().unparse() << std::endl; std::cout << "Raw stream data:" << std::endl; std::cout.flush(); QUtil::binary_stdout(); PointerHolder out = new Pl_StdioFile("raw", stdout); qtest.pipeStreamData(out.getPointer(), false, false, false); std::cout << std::endl << "Uncompressed stream data:" << std::endl; if (qtest.pipeStreamData(0, true, false, false)) { std::cout.flush(); QUtil::binary_stdout(); out = new Pl_StdioFile("filtered", stdout); qtest.pipeStreamData(out.getPointer(), true, false, false); std::cout << std::endl << "End of stream data" << std::endl; } else { std::cout << "Stream data is not filterable." << std::endl; } } else { // Should not happen! std::cout << "/QTest is an unknown object" << std::endl; } std::cout << "unparse: " << qtest.unparse() << std::endl << "unparseResolved: " << qtest.unparseResolved() << std::endl; } else if (n == 2) { // Encrypted file. This test case is designed for a specific // PDF file. QPDFObjectHandle trailer = pdf.getTrailer(); std::cout << trailer.getKey("/Info"). getKey("/CreationDate").getStringValue() << std::endl; std::cout << trailer.getKey("/Info"). getKey("/Producer").getStringValue() << std::endl; QPDFObjectHandle encrypt = trailer.getKey("/Encrypt"); std::cout << encrypt.getKey("/O").unparse() << std::endl; std::cout << encrypt.getKey("/U").unparse() << std::endl; QPDFObjectHandle root = pdf.getRoot(); QPDFObjectHandle pages = root.getKey("/Pages"); QPDFObjectHandle kids = pages.getKey("/Kids"); QPDFObjectHandle page = kids.getArrayItem(1); // second page QPDFObjectHandle contents = page.getKey("/Contents"); QUtil::binary_stdout(); PointerHolder out = new Pl_StdioFile("filtered", stdout); contents.pipeStreamData(out.getPointer(), true, false, false); } else if (n == 3) { QPDFObjectHandle streams = pdf.getTrailer().getKey("/QStreams"); for (int i = 0; i < streams.getArrayNItems(); ++i) { QPDFObjectHandle stream = streams.getArrayItem(i); std::cout << "-- stream " << i << " --" << std::endl; std::cout.flush(); QUtil::binary_stdout(); PointerHolder out = new Pl_StdioFile("tokenized stream", stdout); stream.pipeStreamData(out.getPointer(), true, true, false); } } else if (n == 4) { // Mutability testing: Make /QTest direct recursively, then // copy to /Info. Also make some other mutations so we can // tell the difference and ensure that the original /QTest // isn't effected. QPDFObjectHandle trailer = pdf.getTrailer(); QPDFObjectHandle qtest = trailer.getKey("/QTest"); qtest.makeDirect(); qtest.removeKey("/Subject"); qtest.replaceKey("/Author", QPDFObjectHandle::newString("Mr. Potato Head")); // qtest.A and qtest.B.A were originally the same object. // They no longer are after makeDirect(). Mutate one of them // and ensure the other is not changed. qtest.getKey("/A").setArrayItem(1, QPDFObjectHandle::newInteger(5)); trailer.replaceKey("/Info", pdf.makeIndirectObject(qtest)); QPDFWriter w(pdf, 0); w.setQDFMode(true); w.setStaticID(true); w.write(); // Prevent "done" message from getting appended exit(0); } else if (n == 5) { std::vector pages = pdf.getAllPages(); int pageno = 0; for (std::vector::iterator iter = pages.begin(); iter != pages.end(); ++iter) { QPDFObjectHandle& page = *iter; ++pageno; std::cout << "page " << pageno << ":" << std::endl; std::cout << " images:" << std::endl; std::map images = page.getPageImages(); for (std::map::iterator iter = images.begin(); iter != images.end(); ++iter) { std::string const& name = (*iter).first; QPDFObjectHandle image = (*iter).second; QPDFObjectHandle dict = image.getDict(); int width = dict.getKey("/Width").getIntValue(); int height = dict.getKey("/Height").getIntValue(); std::cout << " " << name << ": " << width << " x " << height << std::endl; } std::cout << " content:" << std::endl; std::vector content = page.getPageContents(); for (std::vector::iterator iter = content.begin(); iter != content.end(); ++iter) { std::cout << " " << (*iter).unparse() << std::endl; } std::cout << "end page " << pageno << std::endl; } QPDFObjectHandle root = pdf.getRoot(); QPDFObjectHandle qstrings = root.getKey("/QStrings"); if (qstrings.isArray()) { std::cout << "QStrings:" << std::endl; int n = qstrings.getArrayNItems(); for (int i = 0; i < n; ++i) { std::cout << qstrings.getArrayItem(i).getUTF8Value() << std::endl; } } QPDFObjectHandle qnumbers = root.getKey("/QNumbers"); if (qnumbers.isArray()) { std::cout << "QNumbers:" << std::endl; int n = qnumbers.getArrayNItems(); for (int i = 0; i < n; ++i) { std::cout << QUtil::double_to_string( qnumbers.getArrayItem(i).getNumericValue(), 3) << std::endl; } } } else if (n == 6) { QPDFObjectHandle root = pdf.getRoot(); QPDFObjectHandle metadata = root.getKey("/Metadata"); if (! metadata.isStream()) { throw std::logic_error("test 6 run on file with no metadata"); } Pl_Buffer bufpl("buffer"); metadata.pipeStreamData(&bufpl, false, false, false); Buffer* buf = bufpl.getBuffer(); unsigned char const* data = buf->getBuffer(); bool cleartext = false; if ((buf->getSize() > 9) && (strncmp((char const*)data, " b = new Buffer(20); unsigned char* bp = b->getBuffer(); memcpy(bp, (char*)"new data for stream\n", 20); // no null! qstream.replaceStreamData( b, QPDFObjectHandle::newNull(), QPDFObjectHandle::newNull()); QPDFWriter w(pdf, "a.pdf"); w.setStaticID(true); w.setStreamDataMode(qpdf_s_preserve); w.write(); } else if (n == 8) { QPDFObjectHandle root = pdf.getRoot(); QPDFObjectHandle qstream = root.getKey("/QStream"); if (! qstream.isStream()) { throw std::logic_error("test 7 run on file with no QStream"); } Pl_Buffer p1("buffer"); Pl_Flate p2("compress", &p1, Pl_Flate::a_deflate); p2.write((unsigned char*)"new data for stream\n", 20); // no null! p2.finish(); PointerHolder b = p1.getBuffer(); // This is a bogus way to use StreamDataProvider, but it does // adequately test its functionality. Provider* provider = new Provider(b); PointerHolder p = provider; qstream.replaceStreamData( p, QPDFObjectHandle::newName("/FlateDecode"), QPDFObjectHandle::newNull(), b->getSize()); provider->badLength(true); try { qstream.getStreamData(); std::cout << "oops -- getStreamData didn't throw" << std::endl; } catch (std::logic_error const& e) { std::cout << "exception: " << e.what() << std::endl; } provider->badLength(false); QPDFWriter w(pdf, "a.pdf"); w.setStaticID(true); w.setStreamDataMode(qpdf_s_preserve); w.write(); } else if (n == 9) { QPDFObjectHandle root = pdf.getRoot(); PointerHolder b1 = new Buffer(20); unsigned char* bp = b1->getBuffer(); memcpy(bp, (char*)"data for new stream\n", 20); // no null! QPDFObjectHandle qstream = QPDFObjectHandle::newStream(&pdf, b1); QPDFObjectHandle rstream = QPDFObjectHandle::newStream(&pdf); try { rstream.getStreamData(); std::cout << "oops -- getStreamData didn't throw" << std::endl; } catch (std::logic_error const& e) { std::cout << "exception: " << e.what() << std::endl; } PointerHolder b2 = new Buffer(22); bp = b2->getBuffer(); memcpy(bp, (char*)"data for other stream\n", 22); // no null! rstream.replaceStreamData( b2, QPDFObjectHandle::newNull(), QPDFObjectHandle::newNull()); root.replaceKey("/QStream", qstream); root.replaceKey("/RStream", rstream); QPDFWriter w(pdf, "a.pdf"); w.setStaticID(true); w.setStreamDataMode(qpdf_s_preserve); w.write(); } else if (n == 10) { PointerHolder b1 = new Buffer(37); unsigned char* bp = b1->getBuffer(); memcpy(bp, (char*)"BT /F1 12 Tf 72 620 Td (Baked) Tj ET\n", 37); PointerHolder b2 = new Buffer(38); bp = b2->getBuffer(); memcpy(bp, (char*)"BT /F1 18 Tf 72 520 Td (Mashed) Tj ET\n", 38); std::vector pages = pdf.getAllPages(); pages[0].addPageContents(QPDFObjectHandle::newStream(&pdf, b1), true); pages[0].addPageContents(QPDFObjectHandle::newStream(&pdf, b2), false); QPDFWriter w(pdf, "a.pdf"); w.setStaticID(true); w.setStreamDataMode(qpdf_s_preserve); w.write(); } else if (n == 11) { QPDFObjectHandle root = pdf.getRoot(); QPDFObjectHandle qstream = root.getKey("/QStream"); PointerHolder b1 = qstream.getStreamData(); PointerHolder b2 = qstream.getRawStreamData(); if ((b1->getSize() == 7) && (memcmp(b1->getBuffer(), "potato\n", 7) == 0)) { std::cout << "filtered stream data okay" << std::endl; } if ((b2->getSize() == 15) && (memcmp(b2->getBuffer(), "706F7461746F0A\n", 15) == 0)) { std::cout << "raw stream data okay" << std::endl; } } else if (n == 12) { pdf.setOutputStreams(0, 0); pdf.showLinearizationData(); } else if (n == 13) { std::ostringstream out; std::ostringstream err; pdf.setOutputStreams(&out, &err); pdf.showLinearizationData(); std::cout << "---output---" << std::endl << out.str() << "---error---" << std::endl << err.str(); } else if (n == 14) { // Exercise swap and replace. This test case is designed for // a specific file. std::vector pages = pdf.getAllPages(); if (pages.size() != 4) { throw std::logic_error("test " + QUtil::int_to_string(n) + " not called 4-page file"); } // Swap pages 2 and 3 pdf.swapObjects(pages[1].getObjectID(), pages[1].getGeneration(), pages[2].getObjectID(), pages[2].getGeneration()); // Replace object and swap objects QPDFObjectHandle trailer = pdf.getTrailer(); QPDFObjectHandle qdict = trailer.getKey("/QDict"); QPDFObjectHandle qarray = trailer.getKey("/QArray"); // Force qdict but not qarray to resolve qdict.isDictionary(); std::map dict_keys; dict_keys["/NewDict"] = QPDFObjectHandle::newInteger(2); QPDFObjectHandle new_dict = QPDFObjectHandle::newDictionary(dict_keys); try { // Do it wrong first... pdf.replaceObject(qdict.getObjectID(), qdict.getGeneration(), qdict); } catch (std::logic_error) { std::cout << "caught logic error as expected" << std::endl; } pdf.replaceObject(qdict.getObjectID(), qdict.getGeneration(), new_dict); // Now qdict still points to the old dictionary std::cout << "old dict: " << qdict.getKey("/Dict").getIntValue() << std::endl; // Swap dict and array pdf.swapObjects(qdict.getObjectID(), qdict.getGeneration(), qarray.getObjectID(), qarray.getGeneration()); // Now qarray will resolve to new object but qdict is still // the old object std::cout << "old dict: " << qdict.getKey("/Dict").getIntValue() << std::endl; std::cout << "new dict: " << qarray.getKey("/NewDict").getIntValue() << std::endl; // Reread qdict, now pointing to an array qdict = pdf.getObjectByID(qdict.getObjectID(), qdict.getGeneration()); std::cout << "swapped array: " << qdict.getArrayItem(0).getName() << std::endl; // Exercise getAsMap and getAsArray std::vector array_elements = qdict.getArrayAsVector(); std::map dict_items = qarray.getDictAsMap(); if ((array_elements.size() == 1) && (array_elements[0].getName() == "/Array") && (dict_items.size() == 1) && (dict_items["/NewDict"].getIntValue() == 2)) { std::cout << "array and dictionary contents are correct" << std::endl; } // Exercise writing to memory buffer QPDFWriter w(pdf); w.setOutputMemory(); w.setStaticID(true); w.setStreamDataMode(qpdf_s_preserve); w.write(); Buffer* b = w.getBuffer(); FILE* f = QUtil::fopen_wrapper(std::string("open a.pdf"), fopen("a.pdf", "wb")); fwrite(b->getBuffer(), b->getSize(), 1, f); fclose(f); delete b; } else { throw std::runtime_error(std::string("invalid test ") + QUtil::int_to_string(n)); } std::cout << "test " << n << " done" << std::endl; } int main(int argc, char* argv[]) { QUtil::setLineBuf(stdout); if ((whoami = strrchr(argv[0], '/')) == NULL) { whoami = argv[0]; } else { ++whoami; } // For libtool's sake.... if (strncmp(whoami, "lt-", 3) == 0) { whoami += 3; } if (argc != 3) { usage(); } try { int n = atoi(argv[1]); char const* filename = argv[2]; runtest(n, filename); } catch (std::exception& e) { std::cerr << e.what() << std::endl; exit(2); } return 0; }