Commit 40ecba4172722533916c359fcfe5a43dcd0801ea
1 parent
cbb26149
Pl_DCT: Use custom source and destination managers (fixes #153)
Avoid calling jpeg_mem_src and jpeg_mem_dest. The custom destination manager writes to the pipeline in smaller chunks to avoid having the whole image in memory at once. The source manager works directly with the Buffer object. Using customer managers avoids use of memory source and destination managers, which are not present in older versions of libjpeg still in use by some Linux distributions.
Showing
4 changed files
with
161 additions
and
44 deletions
libqpdf/Pl_DCT.cc
| 1 | 1 | #include <qpdf/Pl_DCT.hh> |
| 2 | 2 | |
| 3 | 3 | #include <qpdf/QUtil.hh> |
| 4 | +#include <qpdf/QTC.hh> | |
| 4 | 5 | #include <setjmp.h> |
| 5 | 6 | #include <string> |
| 6 | 7 | #include <stdexcept> |
| ... | ... | @@ -80,15 +81,28 @@ Pl_DCT::finish() |
| 80 | 81 | // and decompress causes a memory leak with setjmp/longjmp. Just |
| 81 | 82 | // use a pointer and delete it. |
| 82 | 83 | Buffer* b = this->buf.getBuffer(); |
| 84 | + // The jpeg library is a "C" library, so we use setjmp and longjmp | |
| 85 | + // for exceptoin handling. | |
| 83 | 86 | if (setjmp(jerr.jmpbuf) == 0) |
| 84 | 87 | { |
| 85 | - if (this->action == a_compress) | |
| 88 | + try | |
| 86 | 89 | { |
| 87 | - compress(reinterpret_cast<void*>(&cinfo_compress), b); | |
| 90 | + if (this->action == a_compress) | |
| 91 | + { | |
| 92 | + compress(reinterpret_cast<void*>(&cinfo_compress), b); | |
| 93 | + } | |
| 94 | + else | |
| 95 | + { | |
| 96 | + decompress(reinterpret_cast<void*>(&cinfo_decompress), b); | |
| 97 | + } | |
| 88 | 98 | } |
| 89 | - else | |
| 99 | + catch (std::exception& e) | |
| 90 | 100 | { |
| 91 | - decompress(reinterpret_cast<void*>(&cinfo_decompress), b); | |
| 101 | + // Convert an exception back to a longjmp so we can ensure | |
| 102 | + // that the right cleanup happens. This will get converted | |
| 103 | + // back to an exception. | |
| 104 | + jerr.msg = e.what(); | |
| 105 | + longjmp(jerr.jmpbuf, 1); | |
| 92 | 106 | } |
| 93 | 107 | } |
| 94 | 108 | else |
| ... | ... | @@ -111,24 +125,118 @@ Pl_DCT::finish() |
| 111 | 125 | } |
| 112 | 126 | } |
| 113 | 127 | |
| 114 | -class Freer | |
| 128 | +struct dct_pipeline_dest | |
| 115 | 129 | { |
| 116 | - public: | |
| 117 | - Freer(unsigned char** p) : | |
| 118 | - p(p) | |
| 130 | + struct jpeg_destination_mgr pub; /* public fields */ | |
| 131 | + unsigned char* buffer; | |
| 132 | + size_t size; | |
| 133 | + Pipeline* next; | |
| 134 | +}; | |
| 135 | + | |
| 136 | +static void | |
| 137 | +init_pipeline_destination(j_compress_ptr) | |
| 138 | +{ | |
| 139 | +} | |
| 140 | + | |
| 141 | +static int | |
| 142 | +empty_pipeline_output_buffer(j_compress_ptr cinfo) | |
| 143 | +{ | |
| 144 | + QTC::TC("libtests", "Pl_DCT empty_pipeline_output_buffer"); | |
| 145 | + dct_pipeline_dest* dest = | |
| 146 | + reinterpret_cast<dct_pipeline_dest*>(cinfo->dest); | |
| 147 | + dest->next->write(dest->buffer, dest->size); | |
| 148 | + dest->pub.next_output_byte = dest->buffer; | |
| 149 | + dest->pub.free_in_buffer = dest->size; | |
| 150 | + return TRUE; | |
| 151 | +} | |
| 152 | + | |
| 153 | +static void | |
| 154 | +term_pipeline_destination(j_compress_ptr cinfo) | |
| 155 | +{ | |
| 156 | + QTC::TC("libtests", "Pl_DCT term_pipeline_destination"); | |
| 157 | + dct_pipeline_dest* dest = | |
| 158 | + reinterpret_cast<dct_pipeline_dest*>(cinfo->dest); | |
| 159 | + dest->next->write(dest->buffer, dest->size - dest->pub.free_in_buffer); | |
| 160 | +} | |
| 161 | + | |
| 162 | +static void | |
| 163 | +jpeg_pipeline_dest(j_compress_ptr cinfo, | |
| 164 | + unsigned char* outbuffer, size_t size, | |
| 165 | + Pipeline* next) | |
| 166 | +{ | |
| 167 | + cinfo->dest = static_cast<struct jpeg_destination_mgr *>( | |
| 168 | + (*cinfo->mem->alloc_small)(reinterpret_cast<j_common_ptr>(cinfo), | |
| 169 | + JPOOL_PERMANENT, | |
| 170 | + sizeof(dct_pipeline_dest))); | |
| 171 | + dct_pipeline_dest* dest = | |
| 172 | + reinterpret_cast<dct_pipeline_dest*>(cinfo->dest); | |
| 173 | + dest->pub.init_destination = init_pipeline_destination; | |
| 174 | + dest->pub.empty_output_buffer = empty_pipeline_output_buffer; | |
| 175 | + dest->pub.term_destination = term_pipeline_destination; | |
| 176 | + dest->pub.next_output_byte = dest->buffer = outbuffer; | |
| 177 | + dest->pub.free_in_buffer = dest->size = size; | |
| 178 | + dest->next = next; | |
| 179 | +} | |
| 180 | + | |
| 181 | +static void | |
| 182 | +init_buffer_source(j_decompress_ptr) | |
| 183 | +{ | |
| 184 | +} | |
| 185 | + | |
| 186 | +static int | |
| 187 | +fill_buffer_input_buffer(j_decompress_ptr) | |
| 188 | +{ | |
| 189 | + // The whole JPEG data is expected to reside in the supplied memory | |
| 190 | + // buffer, so any request for more data beyond the given buffer size | |
| 191 | + // is treated as an error. | |
| 192 | + throw std::runtime_error("invalid jpeg data reading from buffer"); | |
| 193 | + return TRUE; | |
| 194 | +} | |
| 195 | + | |
| 196 | +static void | |
| 197 | +skip_buffer_input_data(j_decompress_ptr cinfo, long num_bytes) | |
| 198 | +{ | |
| 199 | + if (num_bytes < 0) | |
| 119 | 200 | { |
| 201 | + throw std::runtime_error( | |
| 202 | + "reading jpeg: jpeg library requested" | |
| 203 | + " skipping a negative number of bytes"); | |
| 120 | 204 | } |
| 121 | - ~Freer() | |
| 205 | + size_t to_skip = static_cast<size_t>(num_bytes); | |
| 206 | + if ((to_skip > 0) && (to_skip <= cinfo->src->bytes_in_buffer)) | |
| 122 | 207 | { |
| 123 | - if (*p) | |
| 124 | - { | |
| 125 | - free(*p); | |
| 126 | - } | |
| 208 | + cinfo->src->next_input_byte += to_skip; | |
| 209 | + cinfo->src->bytes_in_buffer -= to_skip; | |
| 210 | + } | |
| 211 | + else if (to_skip != 0) | |
| 212 | + { | |
| 213 | + cinfo->src->next_input_byte += cinfo->src->bytes_in_buffer; | |
| 214 | + cinfo->src->bytes_in_buffer = 0; | |
| 127 | 215 | } |
| 216 | +} | |
| 128 | 217 | |
| 129 | - private: | |
| 130 | - unsigned char** p; | |
| 131 | -}; | |
| 218 | +static void | |
| 219 | +term_buffer_source(j_decompress_ptr) | |
| 220 | +{ | |
| 221 | +} | |
| 222 | + | |
| 223 | +static void | |
| 224 | +jpeg_buffer_src(j_decompress_ptr cinfo, Buffer* buffer) | |
| 225 | +{ | |
| 226 | + cinfo->src = reinterpret_cast<jpeg_source_mgr *>( | |
| 227 | + (*cinfo->mem->alloc_small)(reinterpret_cast<j_common_ptr>(cinfo), | |
| 228 | + JPOOL_PERMANENT, | |
| 229 | + sizeof(jpeg_source_mgr))); | |
| 230 | + | |
| 231 | + jpeg_source_mgr* src = cinfo->src; | |
| 232 | + src->init_source = init_buffer_source; | |
| 233 | + src->fill_input_buffer = fill_buffer_input_buffer; | |
| 234 | + src->skip_input_data = skip_buffer_input_data; | |
| 235 | + src->resync_to_restart = jpeg_resync_to_restart; /* use default method */ | |
| 236 | + src->term_source = term_buffer_source; | |
| 237 | + src->bytes_in_buffer = buffer->getSize(); | |
| 238 | + src->next_input_byte = buffer->getBuffer(); | |
| 239 | +} | |
| 132 | 240 | |
| 133 | 241 | void |
| 134 | 242 | Pl_DCT::compress(void* cinfo_p, Buffer* b) |
| ... | ... | @@ -146,10 +254,11 @@ Pl_DCT::compress(void* cinfo_p, Buffer* b) |
| 146 | 254 | defined(__clang__)) |
| 147 | 255 | # pragma GCC diagnostic pop |
| 148 | 256 | #endif |
| 149 | - unsigned char* outbuffer = 0; | |
| 150 | - Freer freer(&outbuffer); | |
| 151 | - unsigned long outsize = 0; | |
| 152 | - jpeg_mem_dest(cinfo, &outbuffer, &outsize); | |
| 257 | + static int const BUF_SIZE = 65536; | |
| 258 | + PointerHolder<unsigned char> outbuffer_ph( | |
| 259 | + true, new unsigned char[BUF_SIZE]); | |
| 260 | + unsigned char* outbuffer = outbuffer_ph.getPointer(); | |
| 261 | + jpeg_pipeline_dest(cinfo, outbuffer, BUF_SIZE, this->getNext()); | |
| 153 | 262 | |
| 154 | 263 | cinfo->image_width = this->image_width; |
| 155 | 264 | cinfo->image_height = this->image_height; |
| ... | ... | @@ -182,7 +291,6 @@ Pl_DCT::compress(void* cinfo_p, Buffer* b) |
| 182 | 291 | (void) jpeg_write_scanlines(cinfo, row_pointer, 1); |
| 183 | 292 | } |
| 184 | 293 | jpeg_finish_compress(cinfo); |
| 185 | - this->getNext()->write(outbuffer, outsize); | |
| 186 | 294 | this->getNext()->finish(); |
| 187 | 295 | } |
| 188 | 296 | |
| ... | ... | @@ -202,7 +310,7 @@ Pl_DCT::decompress(void* cinfo_p, Buffer* b) |
| 202 | 310 | defined(__clang__)) |
| 203 | 311 | # pragma GCC diagnostic pop |
| 204 | 312 | #endif |
| 205 | - jpeg_mem_src(cinfo, b->getBuffer(), b->getSize()); | |
| 313 | + jpeg_buffer_src(cinfo, b); | |
| 206 | 314 | |
| 207 | 315 | (void) jpeg_read_header(cinfo, TRUE); |
| 208 | 316 | (void) jpeg_calc_output_dimensions(cinfo); | ... | ... |
libtests/libtests.testcov
libtests/qtest/dct.test
| ... | ... | @@ -16,38 +16,45 @@ my $td = new TestDriver('dct'); |
| 16 | 16 | |
| 17 | 17 | cleanup(); |
| 18 | 18 | |
| 19 | -$td->runtest("compress", | |
| 20 | - {$td->COMMAND => "dct_compress rawdata a.jpg 400 256 gray"}, | |
| 21 | - {$td->STRING => "", $td->EXIT_STATUS => 0}); | |
| 22 | -$td->runtest("decompress", | |
| 23 | - {$td->COMMAND => "dct_uncompress a.jpg out"}, | |
| 24 | - {$td->STRING => "", $td->EXIT_STATUS => 0}); | |
| 25 | -# Compare | |
| 26 | -my @raw = get_data('rawdata'); | |
| 27 | -my @processed = get_data('out'); | |
| 28 | 19 | my $checked_data = 0; |
| 29 | -if ($td->runtest("bytes in data", | |
| 30 | - {$td->STRING => scalar(@processed)}, | |
| 31 | - {$td->STRING => scalar(@raw)})) | |
| 20 | +foreach my $d (['rawdata', '400 256 gray', 0], | |
| 21 | + ['big-rawdata', '1024 576 rgb', 0.2]) | |
| 32 | 22 | { |
| 33 | - my $mismatch = 0; | |
| 34 | - for (my $i = 0; $i < scalar(@raw); ++$i) | |
| 23 | + my ($in, $args, $mismatch_fraction) = @$d; | |
| 24 | + $td->runtest("compress", | |
| 25 | + {$td->COMMAND => "dct_compress $in a.jpg $args"}, | |
| 26 | + {$td->STRING => "", $td->EXIT_STATUS => 0}); | |
| 27 | + $td->runtest("decompress", | |
| 28 | + {$td->COMMAND => "dct_uncompress a.jpg out"}, | |
| 29 | + {$td->STRING => "", $td->EXIT_STATUS => 0}); | |
| 30 | + # Compare | |
| 31 | + my @raw = get_data($in); | |
| 32 | + my @processed = get_data('out'); | |
| 33 | + my $bytes = scalar(@raw); | |
| 34 | + if ($td->runtest("bytes in data", | |
| 35 | + {$td->STRING => scalar(@processed)}, | |
| 36 | + {$td->STRING => $bytes})) | |
| 35 | 37 | { |
| 36 | - $checked_data = 1; | |
| 37 | - my $delta = abs(ord($raw[$i]) - ord($processed[$i])); | |
| 38 | - if ($delta > 10) | |
| 38 | + ++$checked_data; | |
| 39 | + my $mismatch = 0; | |
| 40 | + for (my $i = 0; $i < scalar(@raw); ++$i) | |
| 39 | 41 | { |
| 40 | - ++$mismatch; | |
| 42 | + my $delta = abs(ord($raw[$i]) - ord($processed[$i])); | |
| 43 | + if ($delta > 10) | |
| 44 | + { | |
| 45 | + ++$mismatch; | |
| 46 | + } | |
| 41 | 47 | } |
| 48 | + my $threshold = int($mismatch_fraction * $bytes); | |
| 49 | + $td->runtest("data is close enough", | |
| 50 | + {$td->STRING => $mismatch <= $threshold ? 'pass' : 'fail'}, | |
| 51 | + {$td->STRING => 'pass'}); | |
| 42 | 52 | } |
| 43 | - $td->runtest("data is close enough", | |
| 44 | - {$td->STRING => $mismatch}, | |
| 45 | - {$td->STRING => '0'}); | |
| 46 | 53 | } |
| 47 | 54 | |
| 48 | 55 | cleanup(); |
| 49 | 56 | |
| 50 | -$td->report(3 + $checked_data); | |
| 57 | +$td->report(6 + $checked_data); | |
| 51 | 58 | |
| 52 | 59 | sub cleanup |
| 53 | 60 | { | ... | ... |
libtests/qtest/dct/big-rawdata
0 → 100644
No preview for this file type