Commit ceae9dc1cbd7ed62a0f58c965ed6daa371295967

Authored by Jay Berkenbilt
1 parent ddc6cf0c

Enhance pdf-create example

pdf-create now creates images with different color spaces and encoding
schemes and verifies them for data correctness.
examples/pdf-create.cc
  1 +//
  2 +// This is an example of creating a PDF file from scratch. It
  3 +// illustrates use of several QPDF operations for creating objects and
  4 +// streams. It also serves as an ullstration of how to use
  5 +// StreamDataProvider with different types of filters.
  6 +//
  7 +
1 8 #include <qpdf/QPDF.hh>
2 9 #include <qpdf/QPDFWriter.hh>
3 10 #include <qpdf/QPDFObjectHandle.hh>
4 11 #include <qpdf/QUtil.hh>
  12 +#include <qpdf/Pl_Buffer.hh>
  13 +#include <qpdf/Pl_RunLength.hh>
  14 +#include <qpdf/Pl_DCT.hh>
5 15 #include <iostream>
6 16 #include <string.h>
7 17 #include <stdlib.h>
... ... @@ -13,35 +23,112 @@ static char const* whoami = 0;
13 23 class ImageProvider: public QPDFObjectHandle::StreamDataProvider
14 24 {
15 25 public:
16   - ImageProvider(int width, int height);
  26 + ImageProvider(std::string const& color_space,
  27 + std::string const& filter);
17 28 virtual ~ImageProvider();
18 29 virtual void provideStreamData(int objid, int generation,
19 30 Pipeline* pipeline);
  31 + int getWidth() const;
  32 + int getHeight() const;
20 33  
21 34 private:
22 35 int width;
23   - int height;
  36 + int stripe_height;
  37 + std::string color_space;
  38 + std::string filter;
  39 + int n_stripes;
  40 + std::vector<std::string> stripes;
  41 + J_COLOR_SPACE j_color_space;
24 42 };
25 43  
26   -ImageProvider::ImageProvider(int width, int height) :
27   - width(width),
28   - height(height)
  44 +ImageProvider::ImageProvider(std::string const& color_space,
  45 + std::string const& filter) :
  46 + width(400),
  47 + stripe_height(80),
  48 + color_space(color_space),
  49 + filter(filter),
  50 + n_stripes(6),
  51 + j_color_space(JCS_UNKNOWN)
29 52 {
  53 + if (color_space == "/DeviceCMYK")
  54 + {
  55 + j_color_space = JCS_CMYK;
  56 + stripes.push_back(std::string("\xff\x00\x00\x00", 4));
  57 + stripes.push_back(std::string("\x00\xff\x00\x00", 4));
  58 + stripes.push_back(std::string("\x00\x00\xff\x00", 4));
  59 + stripes.push_back(std::string("\xff\x00\xff\x00", 4));
  60 + stripes.push_back(std::string("\xff\xff\x00\x00", 4));
  61 + stripes.push_back(std::string("\x00\x00\x00\xff", 4));
  62 + }
  63 + else if (color_space == "/DeviceRGB")
  64 + {
  65 + j_color_space = JCS_RGB;
  66 + stripes.push_back(std::string("\xff\x00\x00", 3));
  67 + stripes.push_back(std::string("\x00\xff\x00", 3));
  68 + stripes.push_back(std::string("\x00\x00\xff", 3));
  69 + stripes.push_back(std::string("\xff\x00\xff", 3));
  70 + stripes.push_back(std::string("\xff\xff\x00", 3));
  71 + stripes.push_back(std::string("\x00\x00\x00", 3));
  72 + }
  73 + else if (color_space == "/DeviceGray")
  74 + {
  75 + j_color_space = JCS_GRAYSCALE;
  76 + stripes.push_back(std::string("\xee", 1));
  77 + stripes.push_back(std::string("\xcc", 1));
  78 + stripes.push_back(std::string("\x99", 1));
  79 + stripes.push_back(std::string("\x66", 1));
  80 + stripes.push_back(std::string("\x33", 1));
  81 + stripes.push_back(std::string("\x00", 1));
  82 + }
30 83 }
31 84  
32 85 ImageProvider::~ImageProvider()
33 86 {
34 87 }
35 88  
  89 +int
  90 +ImageProvider::getWidth() const
  91 +{
  92 + return width;
  93 +}
  94 +
  95 +int
  96 +ImageProvider::getHeight() const
  97 +{
  98 + return stripe_height * n_stripes;
  99 +}
  100 +
36 101 void
37 102 ImageProvider::provideStreamData(int objid, int generation,
38 103 Pipeline* pipeline)
39 104 {
40   - for (int i = 0; i < width * height; ++i)
  105 + std::vector<PointerHolder<Pipeline> > to_delete;
  106 + Pipeline* p = pipeline;
  107 +
  108 + if (filter == "/DCTDecode")
  109 + {
  110 + p = new Pl_DCT(
  111 + "image encoder", pipeline,
  112 + width, getHeight(), stripes[0].length(), j_color_space);
  113 + to_delete.push_back(p);
  114 + }
  115 + else if (filter == "/RunLengthDecode")
41 116 {
42   - pipeline->write(QUtil::unsigned_char_pointer("\xff\x7f\x00"), 3);
  117 + p = new Pl_RunLength(
  118 + "image encoder", pipeline, Pl_RunLength::a_encode);
  119 + to_delete.push_back(p);
43 120 }
44   - pipeline->finish();
  121 +
  122 + for (int i = 0; i < n_stripes; ++i)
  123 + {
  124 + for (int j = 0; j < width * stripe_height; ++j)
  125 + {
  126 + p->write(
  127 + QUtil::unsigned_char_pointer(stripes[i].c_str()),
  128 + stripes[i].length());
  129 + }
  130 + }
  131 + p->finish();
45 132 }
46 133  
47 134 void usage()
... ... @@ -56,8 +143,8 @@ static QPDFObjectHandle createPageContents(QPDF&amp; pdf, std::string const&amp; text)
56 143 // Create a stream that displays our image and the given text in
57 144 // our font.
58 145 std::string contents =
59   - "BT /F1 24 Tf 72 720 Td (" + text + ") Tj ET\n"
60   - "q 144 0 0 144 234 324 cm /Im1 Do Q\n";
  146 + "BT /F1 24 Tf 72 320 Td (" + text + ") Tj ET\n"
  147 + "q 244 0 0 144 184 100 cm /Im1 Do Q\n";
61 148 return QPDFObjectHandle::newStream(&pdf, contents);
62 149 }
63 150  
... ... @@ -71,43 +158,34 @@ QPDFObjectHandle newInteger(int val)
71 158 return QPDFObjectHandle::newInteger(val);
72 159 }
73 160  
74   -static void create_pdf(char const* filename)
  161 +void add_page(QPDF& pdf, QPDFObjectHandle font,
  162 + std::string const& color_space,
  163 + std::string const& filter)
75 164 {
76   - QPDF pdf;
77   -
78   - // Start with an empty PDF that has no pages or non-required objects.
79   - pdf.emptyPDF();
80   -
81   - // Add an indirect object to contain a font descriptor for the
82   - // built-in Helvetica font.
83   - QPDFObjectHandle font = pdf.makeIndirectObject(
84   - QPDFObjectHandle::parse(
85   - "<<"
86   - " /Type /Font"
87   - " /Subtype /Type1"
88   - " /Name /F1"
89   - " /BaseFont /Helvetica"
90   - " /Encoding /WinAnsiEncoding"
91   - ">>"));
92   -
93   - // Create a stream to encode our image. We don't have to set the
94   - // length or filters. QPDFWriter will fill in the length and
95   - // compress the stream data using FlateDecode by default.
  165 + // Create a stream to encode our image. QPDFWriter will fill in
  166 + // the length and will respect our filters based on stream data
  167 + // mode. Since we are not specifying, QPDFWriter will compress
  168 + // with /FlateDecode if we don't provide any other form of
  169 + // compression.
  170 + ImageProvider* p = new ImageProvider(color_space, filter);
  171 + PointerHolder<QPDFObjectHandle::StreamDataProvider> provider(p);
  172 + int width = p->getWidth();
  173 + int height = p->getHeight();
96 174 QPDFObjectHandle image = QPDFObjectHandle::newStream(&pdf);
97 175 image.replaceDict(QPDFObjectHandle::parse(
98 176 "<<"
99 177 " /Type /XObject"
100 178 " /Subtype /Image"
101   - " /ColorSpace /DeviceRGB"
102 179 " /BitsPerComponent 8"
103   - " /Width 100"
104   - " /Height 100"
105 180 ">>"));
  181 + QPDFObjectHandle image_dict = image.getDict();
  182 + image_dict.replaceKey("/ColorSpace", newName(color_space));
  183 + image_dict.replaceKey("/Width", newInteger(width));
  184 + image_dict.replaceKey("/Height", newInteger(height));
  185 +
106 186 // Provide the stream data.
107   - ImageProvider* p = new ImageProvider(100, 100);
108   - PointerHolder<QPDFObjectHandle::StreamDataProvider> provider(p);
109 187 image.replaceStreamData(provider,
110   - QPDFObjectHandle::newNull(),
  188 + QPDFObjectHandle::parse(filter),
111 189 QPDFObjectHandle::newNull());
112 190  
113 191 // Create direct objects as needed by the page dictionary.
... ... @@ -129,11 +207,11 @@ static void create_pdf(char const* filename)
129 207 mediabox.appendItem(newInteger(0));
130 208 mediabox.appendItem(newInteger(0));
131 209 mediabox.appendItem(newInteger(612));
132   - mediabox.appendItem(newInteger(792));
  210 + mediabox.appendItem(newInteger(392));
133 211  
134 212 // Create the page content stream
135 213 QPDFObjectHandle contents = createPageContents(
136   - pdf, "Look at the pretty, orange square!");
  214 + pdf, color_space + " with filter " + filter);
137 215  
138 216 // Create the page dictionary
139 217 QPDFObjectHandle page = pdf.makeIndirectObject(
... ... @@ -144,14 +222,191 @@ static void create_pdf(char const* filename)
144 222 page.replaceKey("/Resources", resources);
145 223  
146 224 // Add the page to the PDF file
147   - pdf.addPage(page, true);
  225 + pdf.addPage(page, false);
  226 +}
  227 +
  228 +static void check(char const* filename,
  229 + std::vector<std::string> const& color_spaces,
  230 + std::vector<std::string> const& filters)
  231 +{
  232 + // Each stream is compressed the way it is supposed to be. We will
  233 + // add additional tests in qpdf.test to exercise QPDFWriter more
  234 + // fully. In this case, we want to make sure that we actually have
  235 + // RunLengthDecode and DCTDecode where we are supposed to and
  236 + // FlateDecode where we provided no filters.
  237 +
  238 + // Each image is correct. For non-lossy image compression, the
  239 + // uncompressed image data should exactly match what ImageProvider
  240 + // provided. For the DCTDecode data, allow for some fuzz to handle
  241 + // jpeg compression as well as its variance on different systems.
  242 +
  243 + // These tests should use QPDFObjectHandle's stream data retrieval
  244 + // methods, but don't try to fully exercise them here. That is
  245 + // done elsewhere.
  246 +
  247 + size_t n_color_spaces = color_spaces.size();
  248 + size_t n_filters = filters.size();
  249 +
  250 + QPDF pdf;
  251 + pdf.processFile(filename);
  252 + std::vector<QPDFObjectHandle> const& pages = pdf.getAllPages();
  253 + if (n_color_spaces * n_filters != pages.size())
  254 + {
  255 + throw std::logic_error("incorrect number of pages");
  256 + }
  257 + size_t pageno = 1;
  258 + bool errors = false;
  259 + for (std::vector<QPDFObjectHandle>::const_iterator page_iter =
  260 + pages.begin();
  261 + page_iter != pages.end(); ++page_iter)
  262 + {
  263 + QPDFObjectHandle page = *page_iter;
  264 + std::map<std::string, QPDFObjectHandle> images = page.getPageImages();
  265 + if (images.size() != 1)
  266 + {
  267 + throw std::logic_error("incorrect number of images on page");
  268 + }
  269 +
  270 + // Check filter and color space.
  271 + std::string desired_color_space =
  272 + color_spaces[(pageno - 1) / n_color_spaces];
  273 + std::string desired_filter =
  274 + filters[(pageno - 1) % n_filters];
  275 + // In the default mode, QPDFWriter will compress with
  276 + // /FlateDecode if no filters are provided.
  277 + if (desired_filter == "null")
  278 + {
  279 + desired_filter = "/FlateDecode";
  280 + }
  281 + QPDFObjectHandle image = images.begin()->second;
  282 + QPDFObjectHandle image_dict = image.getDict();
  283 + QPDFObjectHandle color_space = image_dict.getKey("/ColorSpace");
  284 + QPDFObjectHandle filter = image_dict.getKey("/Filter");
  285 + bool this_errors = false;
  286 + if (! (filter.isName() && (filter.getName() == desired_filter)))
  287 + {
  288 + this_errors = errors = true;
  289 + std::cout << "page " << pageno << ": expected filter "
  290 + << desired_filter << "; actual filter = "
  291 + << filter.unparse() << std::endl;
  292 + }
  293 + if (! (color_space.isName() &&
  294 + (color_space.getName() == desired_color_space)))
  295 + {
  296 + this_errors = errors = true;
  297 + std::cout << "page " << pageno << ": expected color space "
  298 + << desired_color_space << "; actual color space = "
  299 + << color_space.unparse() << std::endl;
  300 + }
  301 +
  302 + if (! this_errors)
  303 + {
  304 + // Check image data
  305 + PointerHolder<Buffer> actual_data =
  306 + image.getStreamData(qpdf_dl_all);
  307 + ImageProvider* p = new ImageProvider(desired_color_space, "null");
  308 + PointerHolder<QPDFObjectHandle::StreamDataProvider> provider(p);
  309 + Pl_Buffer b_p("get image data");
  310 + provider->provideStreamData(0, 0, &b_p);
  311 + PointerHolder<Buffer> desired_data(b_p.getBuffer());
  312 +
  313 + if (desired_data->getSize() != actual_data->getSize())
  314 + {
  315 + std::cout << "page " << pageno
  316 + << ": image data length mismatch" << std::endl;
  317 + this_errors = errors = true;
  318 + }
  319 + else
  320 + {
  321 + // Compare bytes. For JPEG, allow a certain number of
  322 + // the bytes to be off desired by more than a given
  323 + // tolerance. Any of the samples may be a little off
  324 + // because of lossy compression, and around sharp
  325 + // edges, things can be quite off. For non-lossy
  326 + // compression, do not allow any tolerance.
  327 + unsigned char const* actual_bytes = actual_data->getBuffer();
  328 + unsigned char const* desired_bytes = desired_data->getBuffer();
  329 + size_t len = actual_data->getSize();
  330 + unsigned int mismatches = 0;
  331 + int tolerance = (
  332 + desired_filter == "/DCTDecode" ? 10 : 0);
  333 + unsigned int threshold = (
  334 + desired_filter == "/DCTDecode" ? len / 40 : 0);
  335 + for (size_t i = 0; i < len; ++i)
  336 + {
  337 + int delta = actual_bytes[i] - desired_bytes[i];
  338 + if ((delta > tolerance) || (delta < -tolerance))
  339 + {
  340 + ++mismatches;
  341 + }
  342 + }
  343 + if (mismatches > threshold)
  344 + {
  345 + std::cout << "page " << pageno
  346 + << ": " << desired_color_space << ", "
  347 + << desired_filter
  348 + << ": mismatches: " << mismatches
  349 + << " of " << len << std::endl;
  350 + this_errors = errors = true;
  351 + }
  352 + }
  353 + }
  354 +
  355 + ++pageno;
  356 + }
  357 + if (errors)
  358 + {
  359 + throw std::logic_error("errors found");
  360 + }
  361 + else
  362 + {
  363 + std::cout << "all checks passed" << std::endl;
  364 + }
  365 +}
  366 +
  367 +static void create_pdf(char const* filename)
  368 +{
  369 + QPDF pdf;
  370 +
  371 + // Start with an empty PDF that has no pages or non-required objects.
  372 + pdf.emptyPDF();
  373 +
  374 + // Add an indirect object to contain a font descriptor for the
  375 + // built-in Helvetica font.
  376 + QPDFObjectHandle font = pdf.makeIndirectObject(
  377 + QPDFObjectHandle::parse(
  378 + "<<"
  379 + " /Type /Font"
  380 + " /Subtype /Type1"
  381 + " /Name /F1"
  382 + " /BaseFont /Helvetica"
  383 + " /Encoding /WinAnsiEncoding"
  384 + ">>"));
  385 +
  386 + std::vector<std::string> color_spaces;
  387 + color_spaces.push_back("/DeviceCMYK");
  388 + color_spaces.push_back("/DeviceRGB");
  389 + color_spaces.push_back("/DeviceGray");
  390 + std::vector<std::string> filters;
  391 + filters.push_back("null");
  392 + filters.push_back("/DCTDecode");
  393 + filters.push_back("/RunLengthDecode");
  394 + for (std::vector<std::string>::iterator c_iter = color_spaces.begin();
  395 + c_iter != color_spaces.end(); ++c_iter)
  396 + {
  397 + for (std::vector<std::string>::iterator f_iter = filters.begin();
  398 + f_iter != filters.end(); ++f_iter)
  399 + {
  400 + add_page(pdf, font, *c_iter, *f_iter);
  401 + }
  402 + }
148 403  
149   - // Write the results. A real application would not call
150   - // setStaticID here, but this example does it for the sake of its
151   - // test suite.
152 404 QPDFWriter w(pdf, filename);
153   - w.setStaticID(true); // for testing only
154 405 w.write();
  406 +
  407 + // For test suite, verify that everything is the way it is
  408 + // supposed to be.
  409 + check(filename, color_spaces, filters);
155 410 }
156 411  
157 412 int main(int argc, char* argv[])
... ...
examples/qtest/create.test
... ... @@ -13,15 +13,12 @@ my $td = new TestDriver(&#39;create&#39;);
13 13  
14 14 $td->runtest("create a simple PDF",
15 15 {$td->COMMAND => "pdf-create a.pdf"},
16   - {$td->STRING => "", $td->EXIT_STATUS => 0});
17   -
18   -$td->runtest("check",
19   - {$td->FILE => "a.pdf"},
20   - {$td->FILE => "orange-square.pdf"});
  16 + {$td->FILE => "create.out", $td->EXIT_STATUS => 0},
  17 + $td->NORMALIZE_NEWLINES);
21 18  
22 19 cleanup();
23 20  
24   -$td->report(2);
  21 +$td->report(1);
25 22  
26 23 sub cleanup
27 24 {
... ...
examples/qtest/create/create.out 0 → 100644
  1 +all checks passed
... ...
examples/qtest/create/orange-square.pdf deleted
No preview for this file type