writepng_init()
Back in the main program we conditionally fill in various elements of our
mainprog_info struct based on the user's command-line options:
interlaced, modtime, have_time, gamma,
bg_red, bg_green, bg_blue, and have_bg.
Note that have_bg is set only if the user provides a background
color and the PNM image type is the experimental ``type 8'' binary
RGBA file. Also, whereas pnmtopng currently requires the user to
provide a text version of the current time for use in the tIME chunk,
wpng automatically determines the current time if
the -time option is given:
if (user_specified_time_option) {
wpng_info.modtime = time(NULL);
wpng_info.have_time = TRUE;
}
After finishing the command-line options, we next open the input file (in
binary mode!), verify that it's in the
proper format, and read its basic parameters: image height, width, and depth.
We also generate an output filename based on the input name and verify both
that the output file does not already exist and that it can be opened and
written to (also in binary mode!). That provides enough information to fill
in most of the rest of mainprog_info: infile, pnmtype,
have_bg, width, height, sample_depth, and
outfile.
If any errors have occurred by this point, wpng prints the usage
screen--including the libraries' version information--and exits.
Otherwise it optionally prompts the user for PNG text information and then, finally,
calls our PNG initialization routine, writepng_init(). It is declared
as follows:
int writepng_init(mainprog_info *mainprog_ptr)
where mainprog_ptr just points at the mainprog_info struct
we filled in in the main program. writepng_init() begins with some
fairly standard libpng boilerplate:
png_structp png_ptr;
png_infop info_ptr;
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
mainprog_ptr, writepng_error_handler, NULL);
if (!png_ptr)
return 4; /* out of memory */
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_write_struct(&png_ptr, NULL);
return 4;
}
This fragment allocates memory for the two internal structures that libpng
currently requires and sets up a custom error handler. Note that while the
structs have the same names and types as those used in our PNG-reading demo
programs, libpng provides separate functions to create and destroy them.
The first function, png_create_write_struct(), also checks that the
compile-time and runtime versions of libpng are reasonably compatible. Of
course, any change to the library may create unforeseen incompatibilities,
so passing this test does not absolutely guarantee that everything will work.
Failing it, on the other hand, is a pretty good indication that things will
break.
The second and third arguments to png_create_write_struct() are the
keys to installing a custom error handler. The second argument is a pointer
to application data (mainprog_ptr, in this case) that will be supplied
to the error handler; the third argument is the custom error-handling routine
itself. I will explain why it is important to use a custom routine as soon
as we take a look at the next section of code.
Once the structs have been allocated, it is necessary to set up the ``receiving
end'' of the error-handling code for this particular function. Essentially
every user function that calls a libpng routine will need code like this; it
amounts to more standard boilerplate, and in general, the only difference
between applications will be where the jmpbuf member is stored. In
this program, as with the one in the previous chapter, we store jmpbuf
in our own struct instead of relying on the one in the main PNG struct:
if (setjmp(mainprog_ptr->jmpbuf)) {
png_destroy_write_struct(&png_ptr, &info_ptr);
return 2;
}
I discussed the semantics of setjmp() and longjmp() in
Chapter 13, "Reading PNG Images"; effectively they amount to a really big goto
statement. The problem is not so much with the precise storage
location of jmpbuf, but rather that its type, jmp_buf,
can be different sizes depending on whether certain sytem macros have
been defined. When one uses the default libpng error handler,
setjmp() is called from the application, but longjmp()
is called from within libpng. Since it is not uncommon for the library
to be compiled separately from the application--indeed, it may not
even have been compiled on the same system--there is no guarantee
that the jmp_buf sizes in libpng and the application will be
consistent. If they are not, mayhem ensues. See the sidebar for a solution.
writepng_error_handler()
The solution is a ``custom'' error handler, though that's a slight misnomer
in our case. Completely custom error handlers can certainly be installed, but
libpng currently assumes that its error-handling routine will never return.
This rather drastically limits the options for alternatives--basically, one
can use longjmp() or exit(), which amounts to an even larger
goto statement.[104]
Here, as in Chapter 14, "Reading PNG Images Progressively", I have merely taken libpng's default error handler and
modified it slightly to use mainprog_ptr instead of png_ptr:
static void writepng_error_handler(png_structp png_ptr,
png_const_charp msg)
{
mainprog_info *mainprog_ptr;
fprintf(stderr, "writepng libpng error: %s\n", msg);
fflush(stderr);
mainprog_ptr = png_get_error_ptr(png_ptr);
if (mainprog_ptr == NULL) {
fprintf(stderr, "writepng severe error: "
"jmpbuf not recoverable; terminating.\n");
fflush(stderr);
exit(99);
}
longjmp(mainprog_ptr->jmpbuf, 1);
}
Because we have to use a libpng function, however trivial, to retrieve our
pointer, there is an extra block of code in our version that makes sure the
pointer is not NULL. If it is, we are completely stuck, and our only real
option is to exit. But assuming the pointer seems valid (it may
have been overwritten with an invalid but non-NULL address, in which case
we're going to ``exit'' whether we want to or not), we use our saved
jmp_buf and longjump back to the part of our application that most
recently invoked setjmp(). The key difference from using libpng's
error handler is simply the location of the longjmp() call. Here
we call both setjmp() and longjmp() within the same
application--indeed, from within the same source file. They are therefore
guaranteed to have consistent notions of how a jmp_buf is
defined, so we have eliminated one more potential source of
very-difficult-to-debug crashes.
|
As long as we're on the subject of alternatives, libpng also supports
user-defined input/output functions. But its default is to read from
or write to PNG files, and since that is precisely what we want to do
here, I chose to stick with the standard I/O-initialization call and pass
the output file's pointer to libpng:
png_init_io(png_ptr, mainprog_ptr->outfile);
Next we deal with compression. libpng has pretty good defaults, and many
programs (possibly most) will not need to do anything here. But in our
case we're converting from an uncompressed image format to PNG; for any
given image, we're unlikely to do so more than once, and even if we convert
many images, wpng is a command-line program and can easily be
incorporated into a script for batch processing. Thus I chose to override
libpng's default compression setting (zlib level 6--see
Chapter 9, "Compression and Filtering")
with the slower ``maximum'' setting (zlib level 9):
png_set_compression_level(png_ptr, Z_BEST_COMPRESSION);
Note that a good PNG-writing program should let the user decide whether and
how to override the default settings; options for very fast saves and/or for
maximal compression might be reasonable, in addition to the default. In
fact, pnmtopng provides options to do just that.
Tweaking Compression
Closely related to compression is filtering, one area in which it
is almost always better to leave the decision up to libpng. Repeated tests
have shown that filtering is almost never useful on palette-based images,
but on everything else it is quite beneficial. Though libpng allows one to
restrict its filter selection, this is rarely a good idea; dynamic filtering
works best when the encoder can choose from the five defined filter
types. But for programmers who want to play with the alternatives, here's
an example:
/*
>>> this is pseudo-code
if (palette image, i.e., don't want filtering) {
png_set_filter(png_ptr, PNG_FILTER_TYPE_BASE,
PNG_FILTER_NONE);
png_set_compression_strategy(png_ptr, Z_DEFAULT_STRATEGY);
} else {
>>> leave default filter selection alone
png_set_compression_strategy(png_ptr, Z_FILTERED);
}
*/
The calls to png_set_compression_strategy() actually alter zlib's
behavior to work better with the filtered output. Other zlib parameters
can also be tweaked, at least in theory; these include the sliding window size,
memory level, and compression method. For the last, only method 8 is currently
defined, but zlib 2.0 is likely to introduce at least one or two new methods
when it is eventually released. Of course, unless and until the PNG
specification is revised accordingly, no new compression method can be used
within a PNG file without invalidating it.
The window size is the only thing a normal PNG encoder should consider
changing, and then only when the total size of the image data, plus
one extra byte per row for the row filters, amounts to 16 kilobytes or
less. In such a case, the encoder can use a smaller power-of-two
window size without affecting compression, which allows decoders to
reduce their memory usage. The following fragment shows how to modify
these zlib parameters; the values shown are the defaults used by
libpng (consult the libpng documentation, specifically ``Configuring
zlib'' and ``Controlling row filtering''):
/*
>>> second arg is power of two; 8 through 15 (256-32768) valid
png_set_compression_window_bits(png_ptr, 15);
png_set_compression_mem_level(png_ptr, 8);
png_set_compression_method(png_ptr, 8);
*/
|
The next step is to convert our notion of the image type into
something libpng will understand. In this case, because we support
only three basic image types--grayscale, RGB, or RGBA--we have a
one-to-one correspondence between input and output types, so setting
the PNG color type is easy. For more general programs, libpng
provides several PNG_COLOR_MASK_* macros that can be combined
to get the color type, with the exception that
PNG_COLOR_MASK_PALETTE and PNG_COLOR_MASK_ALPHA are
incompatible. We also set the appropriate PNG interlace type if the
user so requested:
int color_type, interlace_type;
if (mainprog_ptr->pnmtype == 5)
color_type = PNG_COLOR_TYPE_GRAY;
else if (mainprog_ptr->pnmtype == 6)
color_type = PNG_COLOR_TYPE_RGB;
else if (mainprog_ptr->pnmtype == 8)
color_type = PNG_COLOR_TYPE_RGB_ALPHA;
else {
png_destroy_write_struct(&png_ptr, &info_ptr);
return 11;
}
interlace_type = mainprog_ptr->interlaced? PNG_INTERLACE_ADAM7 :
PNG_INTERLACE_NONE;
At this point, we can set the basic image parameters. We have the option of using several functions, each of which sets a single parameter, but
there is really no point in doing so. Instead we set all of them with
a single call to png_set_IHDR():
png_set_IHDR(png_ptr, info_ptr, mainprog_ptr->width,
mainprog_ptr->height, mainprog_ptr->sample_depth,
color_type, interlace_type,
PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
If we supported palette-based images, this is the point at which we
would define the palette for libpng, via the png_set_PLTE()
and possibly png_set_tRNS() functions. We can also set any optional parameters the user specified, starting with the gamma
value, background color, and image modification time. In the case of
the background color, we know that have_bg will be true only
if the image has an alpha channel; in this program, that
necessarily implies that it's an RGBA image, not grayscale with alpha
or palette-based with transparency. Thus we only fill in the red,
green, and blue elements of the png_color_16 struct:
if (mainprog_ptr->gamma > 0.0)
png_set_gAMA(png_ptr, info_ptr, mainprog_ptr->gamma);
if (mainprog_ptr->have_bg) {
png_color_16 background;
background.red = mainprog_ptr->bg_red;
background.green = mainprog_ptr->bg_green;
background.blue = mainprog_ptr->bg_blue;
png_set_bKGD(png_ptr, info_ptr, &background);
}
if (mainprog_ptr->have_time) {
png_time modtime;
png_convert_from_time_t(&modtime, mainprog_ptr->modtime);
png_set_tIME(png_ptr, info_ptr, &modtime);
}
It is also worth noting that libpng copies most of the data it needs into
its own structs, so we can get away with using temporary variables like
background and modtime without worrying about their values
being corrupted before libpng is ready to write them to the file. The only
exceptions are things involving pointers, in which case libpng copies the
pointer itself but not the buffer to which it points. In fact, libpng's
text-handling code is an excellent example of that:
if (mainprog_ptr->have_text) {
png_text text[6];
int num_text = 0;
if (mainprog_ptr->have_text & TEXT_TITLE) {
text[num_text].compression = PNG_TEXT_COMPRESSION_NONE;
text[num_text].key = "Title";
text[num_text].text = mainprog_ptr->title;
++num_text;
}
if (mainprog_ptr->have_text & TEXT_AUTHOR) {
text[num_text].compression = PNG_TEXT_COMPRESSION_NONE;
text[num_text].key = "Author";
text[num_text].text = mainprog_ptr->author;
++num_text;
}
if (mainprog_ptr->have_text & TEXT_DESC) {
text[num_text].compression = PNG_TEXT_COMPRESSION_NONE;
text[num_text].key = "Description";
text[num_text].text = mainprog_ptr->desc;
++num_text;
}
if (mainprog_ptr->have_text & TEXT_COPY) {
text[num_text].compression = PNG_TEXT_COMPRESSION_NONE;
text[num_text].key = "Copyright";
text[num_text].text = mainprog_ptr->copyright;
++num_text;
}
if (mainprog_ptr->have_text & TEXT_EMAIL) {
text[num_text].compression = PNG_TEXT_COMPRESSION_NONE;
text[num_text].key = "E-mail";
text[num_text].text = mainprog_ptr->email;
++num_text;
}
if (mainprog_ptr->have_text & TEXT_URL) {
text[num_text].compression = PNG_TEXT_COMPRESSION_NONE;
text[num_text].key = "URL";
text[num_text].text = mainprog_ptr->url;
++num_text;
}
png_set_text(png_ptr, info_ptr, text, num_text);
}
Here I have declared a temporary array of six png_text structs,
each of which consists of four elements: compression, key,
text, and text_length. The first of these simply indicates
whether the text chunk is to be compressed (zTXt) or not (tEXt). key
and text are pointers to NULL-terminated strings containing the
keyword and actual text, respectively. These pointers are what libpng copies,
but the text buffers to which they point must remain valid until either
png_write_info() or png_write_end() is called--we'll
return to that point in a moment. The final member of the struct,
text_length, is used internally by libpng; we need not set it
ourselves, since libpng will do so regardless.
Anywhere from one to six of the structs is filled in, depending on whether
the main program set the appropriate bit for each of the six supported
keywords. Then png_set_text() is called, which triggers libpng to
allocate its own text structs and copy our struct data into them.
Alternatively, we could have used a single png_text struct, repeatedly
filling it in and calling png_set_text() for each keyword; libpng
merely chains the copied text structs together, so the net result would have
been the same.
Text Buffers, PNG Structs, and Core Dumps
The issue of libpng's allocation of its own text buffers is worth a closer
look, because it indirectly led to a subtle but fatal bug in a popular
PNG viewer. The program in question was John Bradley's XV, an elegant and
powerful image viewer/converter for the X Window System. Version 3.10a,
released late in 1994 and still the most recent release as of this writing,
had no native PNG support. But because it was available in source-code form,
it was one of the first applications to support the reading and writing of
PNGs, thanks to a patch created by Alexander Lehmann in June 1995 and later
modified by Andreas Dilger and the author of this book.
This patch was originally written to work with libpng 0.71 and zlib 0.93,
beta versions so old they were arguably alpha-level software. At the time,
major functionality was still being added to libpng, and the so-called
modern ``convenience functions'' for modifying libpng parameters did not
exist. As a result, the patch was designed to access the two PNG structs
directly, and later updates to the patch did not completely eliminate this
behavior. In particular, all versions of the patch through 1.2d, released in
June 1996, allocated their own text structs and plugged them directly into
one of the main PNG structs for libpng's use.
Now fast-forward to January 1998, when the final libpng betas were being
released. By this time, libpng provided functions not only to allocate and
destroy the PNG structs, but also to read from them and write to them. In
particular, png_set_text() already existed in its present form; i.e.,
it allocated its own text structs and copied the user-supplied data into
them. But one of the changes in libpng 0.97 involved plugging some small
memory leaks by freeing these libpng-allocated text structs as part of
png_destroy_write_struct(). Unfortunately, libpng had no way to
track whether it had actually allocated the structs in the first place,
and...well, one can see where this is going. First libpng freed the text
structs, then the XV patch--which had allocated them--did so again.
Boom: segmentation fault, core dump, an incomplete PNG file, and no more XV.
The moral of this little story is simple: 1995-era programs had no
choice but to access libpng structs directly, because that was how
libpng was originally written. But modern programs should never do so,
not only because of this particular problem, but also for the several
other reasons detailed in the previous two chapters. Let's say it
again: Accessing libpng structures directly is just plain evil. Don't
do it!
Ye have been warned.
|
The setting of the text chunks is our last piece of non-pixel-related PNG
information, so our next step is to write all chunks up to the first IDAT:
png_write_info(png_ptr, info_ptr);
Doing this flushes any time or text chunks to the output file, and the
corresponding data in the PNG structs is marked so that it is not written
to the file again later. I mentioned earlier that text buffers must remain
valid until either png_write_info() or
png_write_end() is
called, which implies that either one can be used to write text chunks to
the PNG file. This is indeed the case. Had we wished to put all of our
text chunks (or the time chunk) at the end of the
PNG file, we would have called png_write_info()
first, followed by one or both of png_set_tIME()
and png_set_text().
In the case of the latter function,[105]
we could do both--that is, call it with one or more
text structs before calling png_write_info() and then call it
again with one or more new text structs (perhaps a lengthy legal disclaimer
to be stored in a zTXt chunk) afterward. Any calls to png_set_text()
occurring before png_write_info() will be written to the PNG file
before the IDATs; any calls to it after png_write_info() but before
png_write_end() will be written to the PNG file after the IDATs.
And any png_set_text() or png_set_tIME() calls after
png_write_end() will be ignored.
Having completed our pre-IDAT housekeeping, we can now turn to our image-data
transformations. But unlike our PNG-reading demos, most programs that write
PNGs will not require many transformations. In fact, we only call one, and
technically there's no point even in that:
png_set_packing(png_ptr);
This function packs low-bit-depth pixels into bytes. There are no
low-bit-depth RGB and RGBA images; only grayscale and palette images support
bit depths of 1, 2, or 4. But our main program neither counts colors to see
whether a palette-based representation would be possible, nor checks for
valid low-bit-depth grayscale values, and it always sets sample_depth
to 8, so there is currently no possibility of libpng actually being able to
pack any pixels. However, pnmtopng does both, and perhaps a
subsequent revision of wpng will, too.
The only remaining thing for our initialization function to do is to save
copies of the two PNG-struct pointers for passing to libpng functions
later:
mainprog_ptr->png_ptr = png_ptr;
mainprog_ptr->info_ptr = info_ptr;
return 0;
Once again, we could have used global variables instead, but this program
is intended to demonstrate how a multithreaded PNG encoder might be written.
|