diff --git a/examples/pdf-create.cc b/examples/pdf-create.cc index a7b11a50..6fdfde4c 100644 --- a/examples/pdf-create.cc +++ b/examples/pdf-create.cc @@ -1,7 +1,17 @@ +// +// This is an example of creating a PDF file from scratch. It +// illustrates use of several QPDF operations for creating objects and +// streams. It also serves as an ullstration of how to use +// StreamDataProvider with different types of filters. +// + #include #include #include #include +#include +#include +#include #include #include #include @@ -13,35 +23,112 @@ static char const* whoami = 0; class ImageProvider: public QPDFObjectHandle::StreamDataProvider { public: - ImageProvider(int width, int height); + ImageProvider(std::string const& color_space, + std::string const& filter); virtual ~ImageProvider(); virtual void provideStreamData(int objid, int generation, Pipeline* pipeline); + int getWidth() const; + int getHeight() const; private: int width; - int height; + int stripe_height; + std::string color_space; + std::string filter; + int n_stripes; + std::vector stripes; + J_COLOR_SPACE j_color_space; }; -ImageProvider::ImageProvider(int width, int height) : - width(width), - height(height) +ImageProvider::ImageProvider(std::string const& color_space, + std::string const& filter) : + width(400), + stripe_height(80), + color_space(color_space), + filter(filter), + n_stripes(6), + j_color_space(JCS_UNKNOWN) { + if (color_space == "/DeviceCMYK") + { + j_color_space = JCS_CMYK; + stripes.push_back(std::string("\xff\x00\x00\x00", 4)); + stripes.push_back(std::string("\x00\xff\x00\x00", 4)); + stripes.push_back(std::string("\x00\x00\xff\x00", 4)); + stripes.push_back(std::string("\xff\x00\xff\x00", 4)); + stripes.push_back(std::string("\xff\xff\x00\x00", 4)); + stripes.push_back(std::string("\x00\x00\x00\xff", 4)); + } + else if (color_space == "/DeviceRGB") + { + j_color_space = JCS_RGB; + stripes.push_back(std::string("\xff\x00\x00", 3)); + stripes.push_back(std::string("\x00\xff\x00", 3)); + stripes.push_back(std::string("\x00\x00\xff", 3)); + stripes.push_back(std::string("\xff\x00\xff", 3)); + stripes.push_back(std::string("\xff\xff\x00", 3)); + stripes.push_back(std::string("\x00\x00\x00", 3)); + } + else if (color_space == "/DeviceGray") + { + j_color_space = JCS_GRAYSCALE; + stripes.push_back(std::string("\xee", 1)); + stripes.push_back(std::string("\xcc", 1)); + stripes.push_back(std::string("\x99", 1)); + stripes.push_back(std::string("\x66", 1)); + stripes.push_back(std::string("\x33", 1)); + stripes.push_back(std::string("\x00", 1)); + } } ImageProvider::~ImageProvider() { } +int +ImageProvider::getWidth() const +{ + return width; +} + +int +ImageProvider::getHeight() const +{ + return stripe_height * n_stripes; +} + void ImageProvider::provideStreamData(int objid, int generation, Pipeline* pipeline) { - for (int i = 0; i < width * height; ++i) + std::vector > to_delete; + Pipeline* p = pipeline; + + if (filter == "/DCTDecode") { - pipeline->write(QUtil::unsigned_char_pointer("\xff\x7f\x00"), 3); + p = new Pl_DCT( + "image encoder", pipeline, + width, getHeight(), stripes[0].length(), j_color_space); + to_delete.push_back(p); } - pipeline->finish(); + else if (filter == "/RunLengthDecode") + { + p = new Pl_RunLength( + "image encoder", pipeline, Pl_RunLength::a_encode); + to_delete.push_back(p); + } + + for (int i = 0; i < n_stripes; ++i) + { + for (int j = 0; j < width * stripe_height; ++j) + { + p->write( + QUtil::unsigned_char_pointer(stripes[i].c_str()), + stripes[i].length()); + } + } + p->finish(); } void usage() @@ -56,8 +143,8 @@ static QPDFObjectHandle createPageContents(QPDF& pdf, std::string const& text) // Create a stream that displays our image and the given text in // our font. std::string contents = - "BT /F1 24 Tf 72 720 Td (" + text + ") Tj ET\n" - "q 144 0 0 144 234 324 cm /Im1 Do Q\n"; + "BT /F1 24 Tf 72 320 Td (" + text + ") Tj ET\n" + "q 244 0 0 144 184 100 cm /Im1 Do Q\n"; return QPDFObjectHandle::newStream(&pdf, contents); } @@ -71,43 +158,34 @@ QPDFObjectHandle newInteger(int val) return QPDFObjectHandle::newInteger(val); } -static void create_pdf(char const* filename) +void add_page(QPDF& pdf, QPDFObjectHandle font, + std::string const& color_space, + std::string const& filter) { - QPDF pdf; - - // Start with an empty PDF that has no pages or non-required objects. - pdf.emptyPDF(); - - // Add an indirect object to contain a font descriptor for the - // built-in Helvetica font. - QPDFObjectHandle font = pdf.makeIndirectObject( - QPDFObjectHandle::parse( - "<<" - " /Type /Font" - " /Subtype /Type1" - " /Name /F1" - " /BaseFont /Helvetica" - " /Encoding /WinAnsiEncoding" - ">>")); - - // Create a stream to encode our image. We don't have to set the - // length or filters. QPDFWriter will fill in the length and - // compress the stream data using FlateDecode by default. + // Create a stream to encode our image. QPDFWriter will fill in + // the length and will respect our filters based on stream data + // mode. Since we are not specifying, QPDFWriter will compress + // with /FlateDecode if we don't provide any other form of + // compression. + ImageProvider* p = new ImageProvider(color_space, filter); + PointerHolder provider(p); + int width = p->getWidth(); + int height = p->getHeight(); QPDFObjectHandle image = QPDFObjectHandle::newStream(&pdf); image.replaceDict(QPDFObjectHandle::parse( "<<" " /Type /XObject" " /Subtype /Image" - " /ColorSpace /DeviceRGB" " /BitsPerComponent 8" - " /Width 100" - " /Height 100" ">>")); + QPDFObjectHandle image_dict = image.getDict(); + image_dict.replaceKey("/ColorSpace", newName(color_space)); + image_dict.replaceKey("/Width", newInteger(width)); + image_dict.replaceKey("/Height", newInteger(height)); + // Provide the stream data. - ImageProvider* p = new ImageProvider(100, 100); - PointerHolder provider(p); image.replaceStreamData(provider, - QPDFObjectHandle::newNull(), + QPDFObjectHandle::parse(filter), QPDFObjectHandle::newNull()); // Create direct objects as needed by the page dictionary. @@ -129,11 +207,11 @@ static void create_pdf(char const* filename) mediabox.appendItem(newInteger(0)); mediabox.appendItem(newInteger(0)); mediabox.appendItem(newInteger(612)); - mediabox.appendItem(newInteger(792)); + mediabox.appendItem(newInteger(392)); // Create the page content stream QPDFObjectHandle contents = createPageContents( - pdf, "Look at the pretty, orange square!"); + pdf, color_space + " with filter " + filter); // Create the page dictionary QPDFObjectHandle page = pdf.makeIndirectObject( @@ -144,14 +222,191 @@ static void create_pdf(char const* filename) page.replaceKey("/Resources", resources); // Add the page to the PDF file - pdf.addPage(page, true); + pdf.addPage(page, false); +} + +static void check(char const* filename, + std::vector const& color_spaces, + std::vector const& filters) +{ + // Each stream is compressed the way it is supposed to be. We will + // add additional tests in qpdf.test to exercise QPDFWriter more + // fully. In this case, we want to make sure that we actually have + // RunLengthDecode and DCTDecode where we are supposed to and + // FlateDecode where we provided no filters. + + // Each image is correct. For non-lossy image compression, the + // uncompressed image data should exactly match what ImageProvider + // provided. For the DCTDecode data, allow for some fuzz to handle + // jpeg compression as well as its variance on different systems. + + // These tests should use QPDFObjectHandle's stream data retrieval + // methods, but don't try to fully exercise them here. That is + // done elsewhere. + + size_t n_color_spaces = color_spaces.size(); + size_t n_filters = filters.size(); + + QPDF pdf; + pdf.processFile(filename); + std::vector const& pages = pdf.getAllPages(); + if (n_color_spaces * n_filters != pages.size()) + { + throw std::logic_error("incorrect number of pages"); + } + size_t pageno = 1; + bool errors = false; + for (std::vector::const_iterator page_iter = + pages.begin(); + page_iter != pages.end(); ++page_iter) + { + QPDFObjectHandle page = *page_iter; + std::map images = page.getPageImages(); + if (images.size() != 1) + { + throw std::logic_error("incorrect number of images on page"); + } + + // Check filter and color space. + std::string desired_color_space = + color_spaces[(pageno - 1) / n_color_spaces]; + std::string desired_filter = + filters[(pageno - 1) % n_filters]; + // In the default mode, QPDFWriter will compress with + // /FlateDecode if no filters are provided. + if (desired_filter == "null") + { + desired_filter = "/FlateDecode"; + } + QPDFObjectHandle image = images.begin()->second; + QPDFObjectHandle image_dict = image.getDict(); + QPDFObjectHandle color_space = image_dict.getKey("/ColorSpace"); + QPDFObjectHandle filter = image_dict.getKey("/Filter"); + bool this_errors = false; + if (! (filter.isName() && (filter.getName() == desired_filter))) + { + this_errors = errors = true; + std::cout << "page " << pageno << ": expected filter " + << desired_filter << "; actual filter = " + << filter.unparse() << std::endl; + } + if (! (color_space.isName() && + (color_space.getName() == desired_color_space))) + { + this_errors = errors = true; + std::cout << "page " << pageno << ": expected color space " + << desired_color_space << "; actual color space = " + << color_space.unparse() << std::endl; + } + + if (! this_errors) + { + // Check image data + PointerHolder actual_data = + image.getStreamData(qpdf_dl_all); + ImageProvider* p = new ImageProvider(desired_color_space, "null"); + PointerHolder provider(p); + Pl_Buffer b_p("get image data"); + provider->provideStreamData(0, 0, &b_p); + PointerHolder desired_data(b_p.getBuffer()); + + if (desired_data->getSize() != actual_data->getSize()) + { + std::cout << "page " << pageno + << ": image data length mismatch" << std::endl; + this_errors = errors = true; + } + else + { + // Compare bytes. For JPEG, allow a certain number of + // the bytes to be off desired by more than a given + // tolerance. Any of the samples may be a little off + // because of lossy compression, and around sharp + // edges, things can be quite off. For non-lossy + // compression, do not allow any tolerance. + unsigned char const* actual_bytes = actual_data->getBuffer(); + unsigned char const* desired_bytes = desired_data->getBuffer(); + size_t len = actual_data->getSize(); + unsigned int mismatches = 0; + int tolerance = ( + desired_filter == "/DCTDecode" ? 10 : 0); + unsigned int threshold = ( + desired_filter == "/DCTDecode" ? len / 40 : 0); + for (size_t i = 0; i < len; ++i) + { + int delta = actual_bytes[i] - desired_bytes[i]; + if ((delta > tolerance) || (delta < -tolerance)) + { + ++mismatches; + } + } + if (mismatches > threshold) + { + std::cout << "page " << pageno + << ": " << desired_color_space << ", " + << desired_filter + << ": mismatches: " << mismatches + << " of " << len << std::endl; + this_errors = errors = true; + } + } + } + + ++pageno; + } + if (errors) + { + throw std::logic_error("errors found"); + } + else + { + std::cout << "all checks passed" << std::endl; + } +} + +static void create_pdf(char const* filename) +{ + QPDF pdf; + + // Start with an empty PDF that has no pages or non-required objects. + pdf.emptyPDF(); + + // Add an indirect object to contain a font descriptor for the + // built-in Helvetica font. + QPDFObjectHandle font = pdf.makeIndirectObject( + QPDFObjectHandle::parse( + "<<" + " /Type /Font" + " /Subtype /Type1" + " /Name /F1" + " /BaseFont /Helvetica" + " /Encoding /WinAnsiEncoding" + ">>")); + + std::vector color_spaces; + color_spaces.push_back("/DeviceCMYK"); + color_spaces.push_back("/DeviceRGB"); + color_spaces.push_back("/DeviceGray"); + std::vector filters; + filters.push_back("null"); + filters.push_back("/DCTDecode"); + filters.push_back("/RunLengthDecode"); + for (std::vector::iterator c_iter = color_spaces.begin(); + c_iter != color_spaces.end(); ++c_iter) + { + for (std::vector::iterator f_iter = filters.begin(); + f_iter != filters.end(); ++f_iter) + { + add_page(pdf, font, *c_iter, *f_iter); + } + } - // Write the results. A real application would not call - // setStaticID here, but this example does it for the sake of its - // test suite. QPDFWriter w(pdf, filename); - w.setStaticID(true); // for testing only w.write(); + + // For test suite, verify that everything is the way it is + // supposed to be. + check(filename, color_spaces, filters); } int main(int argc, char* argv[]) diff --git a/examples/qtest/create.test b/examples/qtest/create.test index bcdac25c..f83509e2 100644 --- a/examples/qtest/create.test +++ b/examples/qtest/create.test @@ -13,15 +13,12 @@ my $td = new TestDriver('create'); $td->runtest("create a simple PDF", {$td->COMMAND => "pdf-create a.pdf"}, - {$td->STRING => "", $td->EXIT_STATUS => 0}); - -$td->runtest("check", - {$td->FILE => "a.pdf"}, - {$td->FILE => "orange-square.pdf"}); + {$td->FILE => "create.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); cleanup(); -$td->report(2); +$td->report(1); sub cleanup { diff --git a/examples/qtest/create/create.out b/examples/qtest/create/create.out new file mode 100644 index 00000000..c9006083 --- /dev/null +++ b/examples/qtest/create/create.out @@ -0,0 +1 @@ +all checks passed diff --git a/examples/qtest/create/orange-square.pdf b/examples/qtest/create/orange-square.pdf deleted file mode 100644 index 28fb47cd..00000000 Binary files a/examples/qtest/create/orange-square.pdf and /dev/null differ