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