/src/libjxl/lib/extras/enc/jpg.cc
Line | Count | Source |
1 | | // Copyright (c) the JPEG XL Project Authors. All rights reserved. |
2 | | // |
3 | | // Use of this source code is governed by a BSD-style |
4 | | // license that can be found in the LICENSE file. |
5 | | |
6 | | #include "lib/extras/enc/jpg.h" |
7 | | |
8 | | #include <memory> |
9 | | |
10 | | #include "lib/extras/enc/encode.h" |
11 | | |
12 | | #if !JPEGXL_ENABLE_JPEG |
13 | | |
14 | | namespace jxl { |
15 | | namespace extras { |
16 | 0 | std::unique_ptr<Encoder> GetJPEGEncoder() { return nullptr; } |
17 | | } // namespace extras |
18 | | } // namespace jxl |
19 | | |
20 | | #else // JPEGXL_ENABLE_JPEG |
21 | | |
22 | | #include <jxl/codestream_header.h> |
23 | | #include <jxl/color_encoding.h> |
24 | | #include <jxl/types.h> |
25 | | |
26 | | #include <algorithm> |
27 | | #include <array> |
28 | | #include <cmath> |
29 | | #include <cstddef> |
30 | | #include <cstdint> |
31 | | #include <cstdlib> |
32 | | #include <cstring> |
33 | | #include <fstream> |
34 | | #include <sstream> |
35 | | #include <string> |
36 | | #include <utility> |
37 | | #include <vector> |
38 | | |
39 | | #include "lib/extras/exif.h" |
40 | | #include "lib/extras/include_jpeglib.h" |
41 | | #include "lib/extras/packed_image.h" |
42 | | #include "lib/jxl/base/common.h" |
43 | | #include "lib/jxl/base/data_parallel.h" |
44 | | #include "lib/jxl/base/sanitizers.h" |
45 | | #include "lib/jxl/base/status.h" |
46 | | |
47 | | #if JPEGXL_ENABLE_SJPEG |
48 | | #include "sjpeg.h" |
49 | | #include "sjpegi.h" |
50 | | #endif |
51 | | |
52 | | namespace jxl { |
53 | | namespace extras { |
54 | | namespace { |
55 | | |
56 | | constexpr unsigned char kICCSignature[12] = { |
57 | | 0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00}; |
58 | | constexpr int kICCMarker = JPEG_APP0 + 2; |
59 | | constexpr size_t kMaxBytesInMarker = 65533; |
60 | | |
61 | | constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, |
62 | | 0x66, 0x00, 0x00}; |
63 | | constexpr int kExifMarker = JPEG_APP0 + 1; |
64 | | |
65 | | enum class JpegEncoder { |
66 | | kLibJpeg, |
67 | | kSJpeg, |
68 | | }; |
69 | | |
70 | | // Popular jpeg scan scripts |
71 | | // The fields of the individual scans are: |
72 | | // comps_in_scan, component_index[], Ss, Se, Ah, Al |
73 | | constexpr auto kScanScript1 = to_array<jpeg_scan_info>({ |
74 | | {1, {0}, 0, 0, 0, 0}, // |
75 | | {1, {1}, 0, 0, 0, 0}, // |
76 | | {1, {2}, 0, 0, 0, 0}, // |
77 | | {1, {0}, 1, 8, 0, 0}, // |
78 | | {1, {0}, 9, 63, 0, 0}, // |
79 | | {1, {1}, 1, 63, 0, 0}, // |
80 | | {1, {2}, 1, 63, 0, 0} // |
81 | | }); |
82 | | constexpr size_t kNumScans1 = kScanScript1.size(); |
83 | | |
84 | | constexpr auto kScanScript2 = to_array<jpeg_scan_info>({ |
85 | | {1, {0}, 0, 0, 0, 0}, // |
86 | | {1, {1}, 0, 0, 0, 0}, // |
87 | | {1, {2}, 0, 0, 0, 0}, // |
88 | | {1, {0}, 1, 2, 0, 1}, // |
89 | | {1, {0}, 3, 63, 0, 1}, // |
90 | | {1, {0}, 1, 63, 1, 0}, // |
91 | | {1, {1}, 1, 63, 0, 0}, // |
92 | | {1, {2}, 1, 63, 0, 0} // |
93 | | }); |
94 | | constexpr size_t kNumScans2 = kScanScript2.size(); |
95 | | |
96 | | constexpr auto kScanScript3 = to_array<jpeg_scan_info>({ |
97 | | {1, {0}, 0, 0, 0, 0}, // |
98 | | {1, {1}, 0, 0, 0, 0}, // |
99 | | {1, {2}, 0, 0, 0, 0}, // |
100 | | {1, {0}, 1, 63, 0, 2}, // |
101 | | {1, {0}, 1, 63, 2, 1}, // |
102 | | {1, {0}, 1, 63, 1, 0}, // |
103 | | {1, {1}, 1, 63, 0, 0}, // |
104 | | {1, {2}, 1, 63, 0, 0} // |
105 | | }); |
106 | | constexpr size_t kNumScans3 = kScanScript3.size(); |
107 | | |
108 | | constexpr auto kScanScript4 = to_array<jpeg_scan_info>({ |
109 | | {3, {0, 1, 2}, 0, 0, 0, 1}, // |
110 | | {1, {0}, 1, 5, 0, 2}, // |
111 | | {1, {2}, 1, 63, 0, 1}, // |
112 | | {1, {1}, 1, 63, 0, 1}, // |
113 | | {1, {0}, 6, 63, 0, 2}, // |
114 | | {1, {0}, 1, 63, 2, 1}, // |
115 | | {3, {0, 1, 2}, 0, 0, 1, 0}, // |
116 | | {1, {2}, 1, 63, 1, 0}, // |
117 | | {1, {1}, 1, 63, 1, 0}, // |
118 | | {1, {0}, 1, 63, 1, 0} // |
119 | | }); |
120 | | constexpr size_t kNumScans4 = kScanScript4.size(); |
121 | | |
122 | | constexpr auto kScanScript5 = to_array<jpeg_scan_info>({ |
123 | | {3, {0, 1, 2}, 0, 0, 0, 1}, // |
124 | | {1, {0}, 1, 5, 0, 2}, // |
125 | | {1, {1}, 1, 5, 0, 2}, // |
126 | | {1, {2}, 1, 5, 0, 2}, // |
127 | | {1, {1}, 6, 63, 0, 2}, // |
128 | | {1, {2}, 6, 63, 0, 2}, // |
129 | | {1, {0}, 6, 63, 0, 2}, // |
130 | | {1, {0}, 1, 63, 2, 1}, // |
131 | | {1, {1}, 1, 63, 2, 1}, // |
132 | | {1, {2}, 1, 63, 2, 1}, // |
133 | | {3, {0, 1, 2}, 0, 0, 1, 0}, // |
134 | | {1, {0}, 1, 63, 1, 0}, // |
135 | | {1, {1}, 1, 63, 1, 0}, // |
136 | | {1, {2}, 1, 63, 1, 0} // |
137 | | }); |
138 | | constexpr size_t kNumScans5 = kScanScript5.size(); |
139 | | |
140 | | // default progressive mode of jpegli |
141 | | constexpr auto kScanScript6 = to_array<jpeg_scan_info>({ |
142 | | {3, {0, 1, 2}, 0, 0, 0, 0}, // |
143 | | {1, {0}, 1, 2, 0, 0}, // |
144 | | {1, {1}, 1, 2, 0, 0}, // |
145 | | {1, {2}, 1, 2, 0, 0}, // |
146 | | {1, {0}, 3, 63, 0, 2}, // |
147 | | {1, {1}, 3, 63, 0, 2}, // |
148 | | {1, {2}, 3, 63, 0, 2}, // |
149 | | {1, {0}, 3, 63, 2, 1}, // |
150 | | {1, {1}, 3, 63, 2, 1}, // |
151 | | {1, {2}, 3, 63, 2, 1}, // |
152 | | {1, {0}, 3, 63, 1, 0}, // |
153 | | {1, {1}, 3, 63, 1, 0}, // |
154 | | {1, {2}, 3, 63, 1, 0}, // |
155 | | }); |
156 | | constexpr size_t kNumScans6 = kScanScript6.size(); |
157 | | |
158 | | // Adapt RGB scan info to grayscale jpegs. |
159 | | void FilterScanComponents(const jpeg_compress_struct* cinfo, |
160 | | jpeg_scan_info* si) { |
161 | | const int all_comps_in_scan = si->comps_in_scan; |
162 | | si->comps_in_scan = 0; |
163 | | for (int j = 0; j < all_comps_in_scan; ++j) { |
164 | | const int component = si->component_index[j]; |
165 | | if (component < cinfo->input_components) { |
166 | | si->component_index[si->comps_in_scan++] = component; |
167 | | } |
168 | | } |
169 | | } |
170 | | |
171 | | Status SetJpegProgression(int progressive_id, |
172 | | std::vector<jpeg_scan_info>* scan_infos, |
173 | | jpeg_compress_struct* cinfo) { |
174 | | if (progressive_id < 0) { |
175 | | return true; |
176 | | } |
177 | | if (progressive_id == 0) { |
178 | | jpeg_simple_progression(cinfo); |
179 | | return true; |
180 | | } |
181 | | const jpeg_scan_info* kScanScripts[] = { |
182 | | kScanScript1.data(), kScanScript2.data(), kScanScript3.data(), |
183 | | kScanScript4.data(), kScanScript5.data(), kScanScript6.data()}; |
184 | | constexpr auto kNumScans = to_array<size_t>( |
185 | | {kNumScans1, kNumScans2, kNumScans3, kNumScans4, kNumScans5, kNumScans6}); |
186 | | if (progressive_id > static_cast<int>(kNumScans.size())) { |
187 | | return JXL_FAILURE("Unknown jpeg scan script id %d", progressive_id); |
188 | | } |
189 | | const jpeg_scan_info* scan_script = kScanScripts[progressive_id - 1]; |
190 | | const size_t num_scans = kNumScans[progressive_id - 1]; |
191 | | // filter scan script for number of components |
192 | | for (size_t i = 0; i < num_scans; ++i) { |
193 | | jpeg_scan_info scan_info = scan_script[i]; |
194 | | FilterScanComponents(cinfo, &scan_info); |
195 | | if (scan_info.comps_in_scan > 0) { |
196 | | scan_infos->emplace_back(scan_info); |
197 | | } |
198 | | } |
199 | | cinfo->scan_info = scan_infos->data(); |
200 | | cinfo->num_scans = scan_infos->size(); |
201 | | return true; |
202 | | } |
203 | | |
204 | | void WriteICCProfile(jpeg_compress_struct* const cinfo, |
205 | | const std::vector<uint8_t>& icc) { |
206 | | constexpr size_t kMaxIccBytesInMarker = |
207 | | kMaxBytesInMarker - sizeof kICCSignature - 2; |
208 | | const int num_markers = |
209 | | static_cast<int>(DivCeil(icc.size(), kMaxIccBytesInMarker)); |
210 | | size_t begin = 0; |
211 | | for (int current_marker = 0; current_marker < num_markers; ++current_marker) { |
212 | | const size_t length = std::min(kMaxIccBytesInMarker, icc.size() - begin); |
213 | | jpeg_write_m_header( |
214 | | cinfo, kICCMarker, |
215 | | static_cast<unsigned int>(length + sizeof kICCSignature + 2)); |
216 | | for (const unsigned char c : kICCSignature) { |
217 | | jpeg_write_m_byte(cinfo, c); |
218 | | } |
219 | | jpeg_write_m_byte(cinfo, current_marker + 1); |
220 | | jpeg_write_m_byte(cinfo, num_markers); |
221 | | for (size_t i = 0; i < length; ++i) { |
222 | | jpeg_write_m_byte(cinfo, icc[begin]); |
223 | | ++begin; |
224 | | } |
225 | | } |
226 | | } |
227 | | void WriteExif(jpeg_compress_struct* const cinfo, |
228 | | const std::vector<uint8_t>& exif) { |
229 | | jpeg_write_m_header( |
230 | | cinfo, kExifMarker, |
231 | | static_cast<unsigned int>(exif.size() + sizeof kExifSignature)); |
232 | | for (const unsigned char c : kExifSignature) { |
233 | | jpeg_write_m_byte(cinfo, c); |
234 | | } |
235 | | for (uint8_t c : exif) { |
236 | | jpeg_write_m_byte(cinfo, c); |
237 | | } |
238 | | } |
239 | | |
240 | | Status SetChromaSubsampling(const std::string& subsampling, |
241 | | jpeg_compress_struct* const cinfo) { |
242 | | const std::pair<const char*, |
243 | | std::pair<std::array<uint8_t, 3>, std::array<uint8_t, 3> > > |
244 | | options[] = {{"444", {{{1, 1, 1}}, {{1, 1, 1}}}}, |
245 | | {"420", {{{2, 1, 1}}, {{2, 1, 1}}}}, |
246 | | {"422", {{{2, 1, 1}}, {{1, 1, 1}}}}, |
247 | | {"440", {{{1, 1, 1}}, {{2, 1, 1}}}}}; |
248 | | for (const auto& option : options) { |
249 | | if (subsampling == option.first) { |
250 | | for (size_t i = 0; i < 3; i++) { |
251 | | cinfo->comp_info[i].h_samp_factor = option.second.first[i]; |
252 | | cinfo->comp_info[i].v_samp_factor = option.second.second[i]; |
253 | | } |
254 | | return true; |
255 | | } |
256 | | } |
257 | | return false; |
258 | | } |
259 | | |
260 | | struct JpegParams { |
261 | | // Common between sjpeg and libjpeg |
262 | | int quality = 100; |
263 | | std::string chroma_subsampling = "444"; |
264 | | // Libjpeg parameters |
265 | | int progressive_id = -1; |
266 | | bool optimize_coding = true; |
267 | | bool is_xyb = false; |
268 | | // Sjpeg parameters |
269 | | int libjpeg_quality = 0; |
270 | | std::string libjpeg_chroma_subsampling = "444"; |
271 | | float psnr_target = 0; |
272 | | std::string custom_base_quant_fn; |
273 | | float search_q_start = 65.0f; |
274 | | float search_q_min = 1.0f; |
275 | | float search_q_max = 100.0f; |
276 | | int search_max_iters = 20; |
277 | | float search_tolerance = 0.1f; |
278 | | float search_q_precision = 0.01f; |
279 | | float search_first_iter_slope = 3.0f; |
280 | | bool enable_adaptive_quant = true; |
281 | | }; |
282 | | |
283 | | Status EncodeWithLibJpeg(const PackedImage& image, const JxlBasicInfo& info, |
284 | | const std::vector<uint8_t>& icc, |
285 | | std::vector<uint8_t> exif, const JpegParams& params, |
286 | | std::vector<uint8_t>* bytes) { |
287 | | if (BITS_IN_JSAMPLE != 8 || sizeof(JSAMPLE) != 1) { |
288 | | return JXL_FAILURE("Only 8 bit JSAMPLE is supported."); |
289 | | } |
290 | | jpeg_compress_struct cinfo = {}; |
291 | | jpeg_error_mgr jerr; |
292 | | cinfo.err = jpeg_std_error(&jerr); |
293 | | jpeg_create_compress(&cinfo); |
294 | | unsigned char* buffer = nullptr; |
295 | | #ifdef LIBJPEG_TURBO_VERSION |
296 | | unsigned long size = 0; // NOLINT |
297 | | #else |
298 | | size_t size = 0; // NOLINT |
299 | | #endif |
300 | | jpeg_mem_dest(&cinfo, &buffer, &size); |
301 | | cinfo.image_width = image.xsize; |
302 | | cinfo.image_height = image.ysize; |
303 | | cinfo.input_components = info.num_color_channels; |
304 | | cinfo.in_color_space = info.num_color_channels == 1 ? JCS_GRAYSCALE : JCS_RGB; |
305 | | jpeg_set_defaults(&cinfo); |
306 | | cinfo.optimize_coding = static_cast<boolean>(params.optimize_coding); |
307 | | if (cinfo.input_components == 3) { |
308 | | JXL_RETURN_IF_ERROR( |
309 | | SetChromaSubsampling(params.chroma_subsampling, &cinfo)); |
310 | | } |
311 | | if (params.is_xyb) { |
312 | | // Tell libjpeg not to convert XYB data to YCbCr. |
313 | | jpeg_set_colorspace(&cinfo, JCS_RGB); |
314 | | } |
315 | | jpeg_set_quality(&cinfo, params.quality, TRUE); |
316 | | std::vector<jpeg_scan_info> scan_infos; |
317 | | JXL_RETURN_IF_ERROR( |
318 | | SetJpegProgression(params.progressive_id, &scan_infos, &cinfo)); |
319 | | jpeg_start_compress(&cinfo, TRUE); |
320 | | if (!icc.empty()) { |
321 | | WriteICCProfile(&cinfo, icc); |
322 | | } |
323 | | if (!exif.empty()) { |
324 | | ResetExifOrientation(exif); |
325 | | WriteExif(&cinfo, exif); |
326 | | } |
327 | | if (cinfo.input_components > 3 || cinfo.input_components < 0) |
328 | | return JXL_FAILURE("invalid numbers of components"); |
329 | | |
330 | | std::vector<uint8_t> row_bytes(image.stride); |
331 | | const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels()); |
332 | | if (cinfo.num_components == static_cast<int>(image.format.num_channels) && |
333 | | image.format.data_type == JXL_TYPE_UINT8) { |
334 | | for (size_t y = 0; y < image.ysize; ++y) { |
335 | | memcpy(row_bytes.data(), pixels + y * image.stride, image.stride); |
336 | | JSAMPROW row[] = {row_bytes.data()}; |
337 | | jpeg_write_scanlines(&cinfo, row, 1); |
338 | | } |
339 | | } else if (image.format.data_type == JXL_TYPE_UINT8) { |
340 | | for (size_t y = 0; y < image.ysize; ++y) { |
341 | | const uint8_t* image_row = pixels + y * image.stride; |
342 | | for (size_t x = 0; x < image.xsize; ++x) { |
343 | | const uint8_t* image_pixel = image_row + x * image.pixel_stride(); |
344 | | memcpy(&row_bytes[x * cinfo.num_components], image_pixel, |
345 | | cinfo.num_components); |
346 | | } |
347 | | JSAMPROW row[] = {row_bytes.data()}; |
348 | | jpeg_write_scanlines(&cinfo, row, 1); |
349 | | } |
350 | | } else { |
351 | | for (size_t y = 0; y < image.ysize; ++y) { |
352 | | const uint8_t* image_row = pixels + y * image.stride; |
353 | | for (size_t x = 0; x < image.xsize; ++x) { |
354 | | const uint8_t* image_pixel = image_row + x * image.pixel_stride(); |
355 | | for (int c = 0; c < cinfo.num_components; ++c) { |
356 | | uint32_t val16 = (image_pixel[2 * c] << 8) + image_pixel[2 * c + 1]; |
357 | | row_bytes[x * cinfo.num_components + c] = (val16 + 128) / 257; |
358 | | } |
359 | | } |
360 | | JSAMPROW row[] = {row_bytes.data()}; |
361 | | jpeg_write_scanlines(&cinfo, row, 1); |
362 | | } |
363 | | } |
364 | | jpeg_finish_compress(&cinfo); |
365 | | jpeg_destroy_compress(&cinfo); |
366 | | bytes->resize(size); |
367 | | // Compressed image data is initialized by libjpeg, which we are not |
368 | | // instrumenting with msan. |
369 | | msan::UnpoisonMemory(buffer, size); |
370 | | std::copy_n(buffer, size, bytes->data()); |
371 | | std::free(buffer); |
372 | | return true; |
373 | | } |
374 | | |
375 | | #if !JPEGXL_ENABLE_SJPEG |
376 | | |
377 | | Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, |
378 | | const std::vector<uint8_t>& icc, |
379 | | std::vector<uint8_t> exif, const JpegParams& params, |
380 | | std::vector<uint8_t>* bytes) { |
381 | | return JXL_FAILURE("JPEG XL was built without sjpeg support"); |
382 | | } |
383 | | |
384 | | #else // JPEGXL_ENABLE_SJPEG |
385 | | |
386 | | struct MySearchHook : public sjpeg::SearchHook { |
387 | | uint8_t base_tables[2][64]; |
388 | | float q_start; |
389 | | float q_precision; |
390 | | float first_iter_slope; |
391 | | void ReadBaseTables(const std::string& fn) { |
392 | | const uint8_t kJPEGAnnexKMatrices[2][64] = { |
393 | | {16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, |
394 | | 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, |
395 | | 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, |
396 | | 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99}, |
397 | | {17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, |
398 | | 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, |
399 | | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, |
400 | | 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99}}; |
401 | | memcpy(base_tables[0], kJPEGAnnexKMatrices[0], sizeof(base_tables[0])); |
402 | | memcpy(base_tables[1], kJPEGAnnexKMatrices[1], sizeof(base_tables[1])); |
403 | | if (!fn.empty()) { |
404 | | std::ifstream f(fn); |
405 | | std::string line; |
406 | | int idx = 0; |
407 | | while (idx < 128 && std::getline(f, line)) { |
408 | | if (line.empty() || line[0] == '#') continue; |
409 | | std::istringstream line_stream(line); |
410 | | std::string token; |
411 | | while (idx < 128 && std::getline(line_stream, token, ',')) { |
412 | | uint8_t val = std::stoi(token); |
413 | | base_tables[idx / 64][idx % 64] = val; |
414 | | idx++; |
415 | | } |
416 | | } |
417 | | } |
418 | | } |
419 | | bool Setup(const sjpeg::EncoderParam& param) override { |
420 | | sjpeg::SearchHook::Setup(param); |
421 | | q = q_start; |
422 | | return true; |
423 | | } |
424 | | void NextMatrix(int idx, uint8_t dst[64]) override { |
425 | | float factor = (q <= 0) ? 5000.0f |
426 | | : (q < 50.0f) ? 5000.0f / q |
427 | | : (q < 100.0f) ? 2 * (100.0f - q) |
428 | | : 0.0f; |
429 | | sjpeg::SetQuantMatrix(base_tables[idx], factor, dst); |
430 | | } |
431 | | bool Update(float result) override { |
432 | | value = result; |
433 | | if (std::fabs(value - target) < tolerance * target) { |
434 | | return true; |
435 | | } |
436 | | if (value > target) { |
437 | | qmax = q; |
438 | | } else { |
439 | | qmin = q; |
440 | | } |
441 | | if (qmin == qmax) { |
442 | | return true; |
443 | | } |
444 | | const float last_q = q; |
445 | | if (pass == 0) { |
446 | | q += first_iter_slope * |
447 | | (for_size ? 0.1 * std::log(target / value) : (target - value)); |
448 | | q = std::max(qmin, std::min(qmax, q)); |
449 | | } else { |
450 | | q = (qmin + qmax) / 2.; |
451 | | } |
452 | | return (pass > 0 && std::fabs(q - last_q) < q_precision); |
453 | | } |
454 | | ~MySearchHook() override = default; |
455 | | }; |
456 | | |
457 | | Status EncodeWithSJpeg(const PackedImage& image, const JxlBasicInfo& info, |
458 | | const std::vector<uint8_t>& icc, |
459 | | std::vector<uint8_t> exif, const JpegParams& params, |
460 | | std::vector<uint8_t>* bytes) { |
461 | | if (image.format.data_type != JXL_TYPE_UINT8) { |
462 | | return JXL_FAILURE("Unsupported pixel data type"); |
463 | | } |
464 | | if (info.alpha_bits > 0) { |
465 | | return JXL_FAILURE("alpha is not supported"); |
466 | | } |
467 | | sjpeg::EncoderParam param(params.quality); |
468 | | if (!icc.empty()) { |
469 | | param.iccp.assign(icc.begin(), icc.end()); |
470 | | } |
471 | | if (!exif.empty()) { |
472 | | ResetExifOrientation(exif); |
473 | | param.exif.assign(exif.begin(), exif.end()); |
474 | | } |
475 | | if (params.chroma_subsampling == "444") { |
476 | | param.yuv_mode = SJPEG_YUV_444; |
477 | | } else if (params.chroma_subsampling == "420") { |
478 | | param.yuv_mode = SJPEG_YUV_420; |
479 | | } else if (params.chroma_subsampling == "420sharp") { |
480 | | param.yuv_mode = SJPEG_YUV_SHARP; |
481 | | } else { |
482 | | return JXL_FAILURE("sjpeg does not support this chroma subsampling mode"); |
483 | | } |
484 | | param.adaptive_quantization = params.enable_adaptive_quant; |
485 | | std::unique_ptr<MySearchHook> hook; |
486 | | if (params.libjpeg_quality > 0) { |
487 | | JpegParams libjpeg_params; |
488 | | libjpeg_params.quality = params.libjpeg_quality; |
489 | | libjpeg_params.chroma_subsampling = params.libjpeg_chroma_subsampling; |
490 | | std::vector<uint8_t> libjpeg_bytes; |
491 | | JXL_RETURN_IF_ERROR(EncodeWithLibJpeg(image, info, icc, exif, |
492 | | libjpeg_params, &libjpeg_bytes)); |
493 | | param.target_mode = sjpeg::EncoderParam::TARGET_SIZE; |
494 | | param.target_value = libjpeg_bytes.size(); |
495 | | } |
496 | | if (params.psnr_target > 0) { |
497 | | param.target_mode = sjpeg::EncoderParam::TARGET_PSNR; |
498 | | param.target_value = params.psnr_target; |
499 | | } |
500 | | if (param.target_mode != sjpeg::EncoderParam::TARGET_NONE) { |
501 | | param.passes = params.search_max_iters; |
502 | | param.tolerance = params.search_tolerance; |
503 | | param.qmin = params.search_q_min; |
504 | | param.qmax = params.search_q_max; |
505 | | hook = jxl::make_unique<MySearchHook>(); |
506 | | hook->ReadBaseTables(params.custom_base_quant_fn); |
507 | | hook->q_start = params.search_q_start; |
508 | | hook->q_precision = params.search_q_precision; |
509 | | hook->first_iter_slope = params.search_first_iter_slope; |
510 | | param.search_hook = hook.get(); |
511 | | } |
512 | | const size_t num_channels = image.format.num_channels; |
513 | | if (num_channels != 1 && num_channels != 3) { |
514 | | return JXL_FAILURE("sjpeg supports only grayscale and RGB input"); |
515 | | } |
516 | | const size_t stride = image.xsize * num_channels; |
517 | | const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels()); |
518 | | std::string output; |
519 | | if (num_channels == 1) { |
520 | | JXL_RETURN_IF_ERROR(sjpeg::EncodeGray(pixels, image.xsize, image.ysize, |
521 | | stride, param, &output)); |
522 | | } else { |
523 | | JXL_RETURN_IF_ERROR(sjpeg::Encode(pixels, image.xsize, image.ysize, stride, |
524 | | param, &output)); |
525 | | } |
526 | | bytes->assign( |
527 | | reinterpret_cast<const uint8_t*>(output.data()), |
528 | | reinterpret_cast<const uint8_t*>(output.data() + output.size())); |
529 | | return true; |
530 | | } |
531 | | |
532 | | #endif // JPEGXL_ENABLE_SJPEG |
533 | | |
534 | | Status EncodeImageJPG(const PackedImage& image, const JxlBasicInfo& info, |
535 | | const std::vector<uint8_t>& icc, |
536 | | std::vector<uint8_t> exif, JpegEncoder encoder, |
537 | | const JpegParams& params, ThreadPool* pool, |
538 | | std::vector<uint8_t>* bytes) { |
539 | | if (params.quality > 100) { |
540 | | return JXL_FAILURE("please specify a 0-100 JPEG quality"); |
541 | | } |
542 | | |
543 | | switch (encoder) { |
544 | | case JpegEncoder::kLibJpeg: |
545 | | JXL_RETURN_IF_ERROR( |
546 | | EncodeWithLibJpeg(image, info, icc, std::move(exif), params, bytes)); |
547 | | break; |
548 | | case JpegEncoder::kSJpeg: |
549 | | JXL_RETURN_IF_ERROR( |
550 | | EncodeWithSJpeg(image, info, icc, std::move(exif), params, bytes)); |
551 | | break; |
552 | | default: |
553 | | return JXL_FAILURE("tried to use an unknown JPEG encoder"); |
554 | | } |
555 | | |
556 | | return true; |
557 | | } |
558 | | |
559 | | class JPEGEncoder : public Encoder { |
560 | | std::vector<JxlPixelFormat> AcceptedFormats() const override { |
561 | | std::vector<JxlPixelFormat> formats; |
562 | | for (const uint32_t num_channels : {1, 2, 3, 4}) { |
563 | | for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { |
564 | | formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, |
565 | | /*data_type=*/JXL_TYPE_UINT8, |
566 | | /*endianness=*/endianness, |
567 | | /*align=*/0}); |
568 | | } |
569 | | formats.push_back(JxlPixelFormat{/*num_channels=*/num_channels, |
570 | | /*data_type=*/JXL_TYPE_UINT16, |
571 | | /*endianness=*/JXL_BIG_ENDIAN, |
572 | | /*align=*/0}); |
573 | | } |
574 | | return formats; |
575 | | } |
576 | | Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, |
577 | | ThreadPool* pool) const override { |
578 | | JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); |
579 | | JpegEncoder jpeg_encoder = JpegEncoder::kLibJpeg; |
580 | | JpegParams params; |
581 | | for (const auto& it : options()) { |
582 | | if (it.first == "q") { |
583 | | std::istringstream is(it.second); |
584 | | JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.quality)); |
585 | | } else if (it.first == "libjpeg_quality") { |
586 | | std::istringstream is(it.second); |
587 | | JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.libjpeg_quality)); |
588 | | } else if (it.first == "chroma_subsampling") { |
589 | | params.chroma_subsampling = it.second; |
590 | | } else if (it.first == "libjpeg_chroma_subsampling") { |
591 | | params.libjpeg_chroma_subsampling = it.second; |
592 | | } else if (it.first == "jpeg_encoder") { |
593 | | if (it.second == "libjpeg") { |
594 | | jpeg_encoder = JpegEncoder::kLibJpeg; |
595 | | } else if (it.second == "sjpeg") { |
596 | | jpeg_encoder = JpegEncoder::kSJpeg; |
597 | | } else { |
598 | | return JXL_FAILURE("unknown jpeg encoder \"%s\"", it.second.c_str()); |
599 | | } |
600 | | } else if (it.first == "progressive") { |
601 | | std::istringstream is(it.second); |
602 | | JXL_RETURN_IF_ERROR(static_cast<bool>(is >> params.progressive_id)); |
603 | | } else if (it.first == "optimize" && it.second == "OFF") { |
604 | | params.optimize_coding = false; |
605 | | } else if (it.first == "adaptive_q" && it.second == "OFF") { |
606 | | params.enable_adaptive_quant = false; |
607 | | } else if (it.first == "psnr") { |
608 | | params.psnr_target = std::stof(it.second); |
609 | | } else if (it.first == "base_quant_fn") { |
610 | | params.custom_base_quant_fn = it.second; |
611 | | } else if (it.first == "search_q_start") { |
612 | | params.search_q_start = std::stof(it.second); |
613 | | } else if (it.first == "search_q_min") { |
614 | | params.search_q_min = std::stof(it.second); |
615 | | } else if (it.first == "search_q_max") { |
616 | | params.search_q_max = std::stof(it.second); |
617 | | } else if (it.first == "search_max_iters") { |
618 | | params.search_max_iters = std::stoi(it.second); |
619 | | } else if (it.first == "search_tolerance") { |
620 | | params.search_tolerance = std::stof(it.second); |
621 | | } else if (it.first == "search_q_precision") { |
622 | | params.search_q_precision = std::stof(it.second); |
623 | | } else if (it.first == "search_first_iter_slope") { |
624 | | params.search_first_iter_slope = std::stof(it.second); |
625 | | } |
626 | | } |
627 | | params.is_xyb = (ppf.color_encoding.color_space == JXL_COLOR_SPACE_XYB); |
628 | | encoded_image->bitstreams.clear(); |
629 | | encoded_image->bitstreams.reserve(ppf.frames.size()); |
630 | | for (const auto& frame : ppf.frames) { |
631 | | JXL_RETURN_IF_ERROR(VerifyPackedImage(frame.color, ppf.info)); |
632 | | encoded_image->bitstreams.emplace_back(); |
633 | | JXL_RETURN_IF_ERROR(EncodeImageJPG( |
634 | | frame.color, ppf.info, ppf.icc, ppf.metadata.exif, jpeg_encoder, |
635 | | params, pool, &encoded_image->bitstreams.back())); |
636 | | } |
637 | | return true; |
638 | | } |
639 | | }; |
640 | | |
641 | | } // namespace |
642 | | |
643 | | std::unique_ptr<Encoder> GetJPEGEncoder() { |
644 | | return jxl::make_unique<JPEGEncoder>(); |
645 | | } |
646 | | |
647 | | } // namespace extras |
648 | | } // namespace jxl |
649 | | |
650 | | #endif // JPEGXL_ENABLE_JPEG |