aboutsummaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2017-08-19 13:10:48 +0200
committerJay Berkenbilt <ejb@ql.org>2017-08-21 23:44:22 +0200
commitceae9dc1cbd7ed62a0f58c965ed6daa371295967 (patch)
tree6df70ecfee76d263781b2ebdcdfb1bcbbdf65370 /examples
parentddc6cf0cf6c11bb50f9f576bf547df3674142c97 (diff)
downloadqpdf-ceae9dc1cbd7ed62a0f58c965ed6daa371295967.tar.zst
Enhance pdf-create example
pdf-create now creates images with different color spaces and encoding schemes and verifies them for data correctness.
Diffstat (limited to 'examples')
-rw-r--r--examples/pdf-create.cc343
-rw-r--r--examples/qtest/create.test9
-rw-r--r--examples/qtest/create/create.out1
-rw-r--r--examples/qtest/create/orange-square.pdfbin1088 -> 0 bytes
4 files changed, 303 insertions, 50 deletions
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 <qpdf/QPDF.hh>
#include <qpdf/QPDFWriter.hh>
#include <qpdf/QPDFObjectHandle.hh>
#include <qpdf/QUtil.hh>
+#include <qpdf/Pl_Buffer.hh>
+#include <qpdf/Pl_RunLength.hh>
+#include <qpdf/Pl_DCT.hh>
#include <iostream>
#include <string.h>
#include <stdlib.h>
@@ -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<std::string> 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<PointerHolder<Pipeline> > to_delete;
+ Pipeline* p = pipeline;
+
+ if (filter == "/DCTDecode")
+ {
+ p = new Pl_DCT(
+ "image encoder", pipeline,
+ width, getHeight(), stripes[0].length(), j_color_space);
+ to_delete.push_back(p);
+ }
+ else if (filter == "/RunLengthDecode")
{
- pipeline->write(QUtil::unsigned_char_pointer("\xff\x7f\x00"), 3);
+ p = new Pl_RunLength(
+ "image encoder", pipeline, Pl_RunLength::a_encode);
+ to_delete.push_back(p);
}
- pipeline->finish();
+
+ 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<QPDFObjectHandle::StreamDataProvider> 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<QPDFObjectHandle::StreamDataProvider> 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<std::string> const& color_spaces,
+ std::vector<std::string> 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<QPDFObjectHandle> 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<QPDFObjectHandle>::const_iterator page_iter =
+ pages.begin();
+ page_iter != pages.end(); ++page_iter)
+ {
+ QPDFObjectHandle page = *page_iter;
+ std::map<std::string, QPDFObjectHandle> 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<Buffer> actual_data =
+ image.getStreamData(qpdf_dl_all);
+ ImageProvider* p = new ImageProvider(desired_color_space, "null");
+ PointerHolder<QPDFObjectHandle::StreamDataProvider> provider(p);
+ Pl_Buffer b_p("get image data");
+ provider->provideStreamData(0, 0, &b_p);
+ PointerHolder<Buffer> 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<std::string> color_spaces;
+ color_spaces.push_back("/DeviceCMYK");
+ color_spaces.push_back("/DeviceRGB");
+ color_spaces.push_back("/DeviceGray");
+ std::vector<std::string> filters;
+ filters.push_back("null");
+ filters.push_back("/DCTDecode");
+ filters.push_back("/RunLengthDecode");
+ for (std::vector<std::string>::iterator c_iter = color_spaces.begin();
+ c_iter != color_spaces.end(); ++c_iter)
+ {
+ for (std::vector<std::string>::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
--- a/examples/qtest/create/orange-square.pdf
+++ /dev/null
Binary files differ