mirror of
https://github.com/qpdf/qpdf.git
synced 2024-12-22 10:58:58 +00:00
Enhance pdf-create example
pdf-create now creates images with different color spaces and encoding schemes and verifies them for data correctness.
This commit is contained in:
parent
ddc6cf0cf6
commit
ceae9dc1cb
@ -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")
|
||||
{
|
||||
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<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[])
|
||||
|
@ -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
|
||||
{
|
||||
|
1
examples/qtest/create/create.out
Normal file
1
examples/qtest/create/create.out
Normal file
@ -0,0 +1 @@
|
||||
all checks passed
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user