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 #include <qpdf/QPDF.hh> 8 #include <qpdf/QPDF.hh>
2 #include <qpdf/QPDFWriter.hh> 9 #include <qpdf/QPDFWriter.hh>
3 #include <qpdf/QPDFObjectHandle.hh> 10 #include <qpdf/QPDFObjectHandle.hh>
4 #include <qpdf/QUtil.hh> 11 #include <qpdf/QUtil.hh>
  12 +#include <qpdf/Pl_Buffer.hh>
  13 +#include <qpdf/Pl_RunLength.hh>
  14 +#include <qpdf/Pl_DCT.hh>
5 #include <iostream> 15 #include <iostream>
6 #include <string.h> 16 #include <string.h>
7 #include <stdlib.h> 17 #include <stdlib.h>
@@ -13,35 +23,112 @@ static char const* whoami = 0; @@ -13,35 +23,112 @@ static char const* whoami = 0;
13 class ImageProvider: public QPDFObjectHandle::StreamDataProvider 23 class ImageProvider: public QPDFObjectHandle::StreamDataProvider
14 { 24 {
15 public: 25 public:
16 - ImageProvider(int width, int height); 26 + ImageProvider(std::string const& color_space,
  27 + std::string const& filter);
17 virtual ~ImageProvider(); 28 virtual ~ImageProvider();
18 virtual void provideStreamData(int objid, int generation, 29 virtual void provideStreamData(int objid, int generation,
19 Pipeline* pipeline); 30 Pipeline* pipeline);
  31 + int getWidth() const;
  32 + int getHeight() const;
20 33
21 private: 34 private:
22 int width; 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 ImageProvider::~ImageProvider() 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 void 101 void
37 ImageProvider::provideStreamData(int objid, int generation, 102 ImageProvider::provideStreamData(int objid, int generation,
38 Pipeline* pipeline) 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 void usage() 134 void usage()
@@ -56,8 +143,8 @@ static QPDFObjectHandle createPageContents(QPDF&amp; pdf, std::string const&amp; text) @@ -56,8 +143,8 @@ static QPDFObjectHandle createPageContents(QPDF&amp; pdf, std::string const&amp; text)
56 // Create a stream that displays our image and the given text in 143 // Create a stream that displays our image and the given text in
57 // our font. 144 // our font.
58 std::string contents = 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 return QPDFObjectHandle::newStream(&pdf, contents); 148 return QPDFObjectHandle::newStream(&pdf, contents);
62 } 149 }
63 150
@@ -71,43 +158,34 @@ QPDFObjectHandle newInteger(int val) @@ -71,43 +158,34 @@ QPDFObjectHandle newInteger(int val)
71 return QPDFObjectHandle::newInteger(val); 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 QPDFObjectHandle image = QPDFObjectHandle::newStream(&pdf); 174 QPDFObjectHandle image = QPDFObjectHandle::newStream(&pdf);
97 image.replaceDict(QPDFObjectHandle::parse( 175 image.replaceDict(QPDFObjectHandle::parse(
98 "<<" 176 "<<"
99 " /Type /XObject" 177 " /Type /XObject"
100 " /Subtype /Image" 178 " /Subtype /Image"
101 - " /ColorSpace /DeviceRGB"  
102 " /BitsPerComponent 8" 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 // Provide the stream data. 186 // Provide the stream data.
107 - ImageProvider* p = new ImageProvider(100, 100);  
108 - PointerHolder<QPDFObjectHandle::StreamDataProvider> provider(p);  
109 image.replaceStreamData(provider, 187 image.replaceStreamData(provider,
110 - QPDFObjectHandle::newNull(), 188 + QPDFObjectHandle::parse(filter),
111 QPDFObjectHandle::newNull()); 189 QPDFObjectHandle::newNull());
112 190
113 // Create direct objects as needed by the page dictionary. 191 // Create direct objects as needed by the page dictionary.
@@ -129,11 +207,11 @@ static void create_pdf(char const* filename) @@ -129,11 +207,11 @@ static void create_pdf(char const* filename)
129 mediabox.appendItem(newInteger(0)); 207 mediabox.appendItem(newInteger(0));
130 mediabox.appendItem(newInteger(0)); 208 mediabox.appendItem(newInteger(0));
131 mediabox.appendItem(newInteger(612)); 209 mediabox.appendItem(newInteger(612));
132 - mediabox.appendItem(newInteger(792)); 210 + mediabox.appendItem(newInteger(392));
133 211
134 // Create the page content stream 212 // Create the page content stream
135 QPDFObjectHandle contents = createPageContents( 213 QPDFObjectHandle contents = createPageContents(
136 - pdf, "Look at the pretty, orange square!"); 214 + pdf, color_space + " with filter " + filter);
137 215
138 // Create the page dictionary 216 // Create the page dictionary
139 QPDFObjectHandle page = pdf.makeIndirectObject( 217 QPDFObjectHandle page = pdf.makeIndirectObject(
@@ -144,14 +222,191 @@ static void create_pdf(char const* filename) @@ -144,14 +222,191 @@ static void create_pdf(char const* filename)
144 page.replaceKey("/Resources", resources); 222 page.replaceKey("/Resources", resources);
145 223
146 // Add the page to the PDF file 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 QPDFWriter w(pdf, filename); 404 QPDFWriter w(pdf, filename);
153 - w.setStaticID(true); // for testing only  
154 w.write(); 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 int main(int argc, char* argv[]) 412 int main(int argc, char* argv[])
examples/qtest/create.test
@@ -13,15 +13,12 @@ my $td = new TestDriver(&#39;create&#39;); @@ -13,15 +13,12 @@ my $td = new TestDriver(&#39;create&#39;);
13 13
14 $td->runtest("create a simple PDF", 14 $td->runtest("create a simple PDF",
15 {$td->COMMAND => "pdf-create a.pdf"}, 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 cleanup(); 19 cleanup();
23 20
24 -$td->report(2); 21 +$td->report(1);
25 22
26 sub cleanup 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