/src/libjxl/lib/jxl/encode.cc
Line | Count | Source (jump to first uncovered line) |
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 "jxl/encode.h" |
7 | | |
8 | | #include <brotli/encode.h> |
9 | | |
10 | | #include <algorithm> |
11 | | #include <cstddef> |
12 | | #include <cstring> |
13 | | |
14 | | #include "jxl/codestream_header.h" |
15 | | #include "jxl/types.h" |
16 | | #include "lib/jxl/aux_out.h" |
17 | | #include "lib/jxl/base/byte_order.h" |
18 | | #include "lib/jxl/base/span.h" |
19 | | #include "lib/jxl/codec_in_out.h" |
20 | | #include "lib/jxl/enc_color_management.h" |
21 | | #include "lib/jxl/enc_external_image.h" |
22 | | #include "lib/jxl/enc_file.h" |
23 | | #include "lib/jxl/enc_icc_codec.h" |
24 | | #include "lib/jxl/encode_internal.h" |
25 | | #include "lib/jxl/exif.h" |
26 | | #include "lib/jxl/jpeg/enc_jpeg_data.h" |
27 | | #include "lib/jxl/sanitizers.h" |
28 | | |
29 | | // Debug-printing failure macro similar to JXL_FAILURE, but for the status code |
30 | | // JXL_ENC_ERROR |
31 | | #ifdef JXL_CRASH_ON_ERROR |
32 | | #define JXL_API_ERROR(enc, error_code, format, ...) \ |
33 | | (enc->error = error_code, \ |
34 | | ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ |
35 | | ::jxl::Abort(), JXL_ENC_ERROR) |
36 | | #define JXL_API_ERROR_NOSET(format, ...) \ |
37 | | (::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ |
38 | | ::jxl::Abort(), JXL_ENC_ERROR) |
39 | | #else // JXL_CRASH_ON_ERROR |
40 | | #define JXL_API_ERROR(enc, error_code, format, ...) \ |
41 | 0 | (enc->error = error_code, \ |
42 | 0 | ((JXL_DEBUG_ON_ERROR) && \ |
43 | 0 | ::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__)), \ |
44 | 0 | JXL_ENC_ERROR) |
45 | | #define JXL_API_ERROR_NOSET(format, ...) \ |
46 | 0 | (::jxl::Debug(("%s:%d: " format "\n"), __FILE__, __LINE__, ##__VA_ARGS__), \ |
47 | 0 | JXL_ENC_ERROR) |
48 | | #endif // JXL_CRASH_ON_ERROR |
49 | | |
50 | | namespace jxl {} // namespace jxl |
51 | | |
52 | 0 | uint32_t JxlEncoderVersion(void) { |
53 | 0 | return JPEGXL_MAJOR_VERSION * 1000000 + JPEGXL_MINOR_VERSION * 1000 + |
54 | 0 | JPEGXL_PATCH_VERSION; |
55 | 0 | } |
56 | | |
57 | | namespace { |
58 | | template <typename T> |
59 | 0 | void AppendJxlpBoxCounter(uint32_t counter, bool last, T* output) { |
60 | 0 | if (last) counter |= 0x80000000; |
61 | 0 | for (size_t i = 0; i < 4; i++) { |
62 | 0 | output->push_back(counter >> (8 * (3 - i)) & 0xff); |
63 | 0 | } |
64 | 0 | } |
65 | | |
66 | | void QueueFrame( |
67 | | const JxlEncoderFrameSettings* frame_settings, |
68 | 92 | jxl::MemoryManagerUniquePtr<jxl::JxlEncoderQueuedFrame>& frame) { |
69 | 92 | if (frame_settings->values.lossless) { |
70 | 0 | frame->option_values.cparams.SetLossless(); |
71 | 0 | } |
72 | | |
73 | 92 | jxl::JxlEncoderQueuedInput queued_input(frame_settings->enc->memory_manager); |
74 | 92 | queued_input.frame = std::move(frame); |
75 | 92 | frame_settings->enc->input_queue.emplace_back(std::move(queued_input)); |
76 | 92 | frame_settings->enc->num_queued_frames++; |
77 | 92 | } |
78 | | |
79 | | void QueueBox(JxlEncoder* enc, |
80 | 0 | jxl::MemoryManagerUniquePtr<jxl::JxlEncoderQueuedBox>& box) { |
81 | 0 | jxl::JxlEncoderQueuedInput queued_input(enc->memory_manager); |
82 | 0 | queued_input.box = std::move(box); |
83 | 0 | enc->input_queue.emplace_back(std::move(queued_input)); |
84 | 0 | enc->num_queued_boxes++; |
85 | 0 | } |
86 | | |
87 | | // TODO(lode): share this code and the Brotli compression code in enc_jpeg_data |
88 | | JxlEncoderStatus BrotliCompress(int quality, const uint8_t* in, size_t in_size, |
89 | 0 | jxl::PaddedBytes* out) { |
90 | 0 | std::unique_ptr<BrotliEncoderState, decltype(BrotliEncoderDestroyInstance)*> |
91 | 0 | enc(BrotliEncoderCreateInstance(nullptr, nullptr, nullptr), |
92 | 0 | BrotliEncoderDestroyInstance); |
93 | 0 | if (!enc) return JXL_API_ERROR_NOSET("BrotliEncoderCreateInstance failed"); |
94 | | |
95 | 0 | BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_QUALITY, quality); |
96 | 0 | BrotliEncoderSetParameter(enc.get(), BROTLI_PARAM_SIZE_HINT, in_size); |
97 | |
|
98 | 0 | constexpr size_t kBufferSize = 128 * 1024; |
99 | 0 | jxl::PaddedBytes temp_buffer(kBufferSize); |
100 | |
|
101 | 0 | size_t avail_in = in_size; |
102 | 0 | const uint8_t* next_in = in; |
103 | |
|
104 | 0 | size_t total_out = 0; |
105 | |
|
106 | 0 | for (;;) { |
107 | 0 | size_t avail_out = kBufferSize; |
108 | 0 | uint8_t* next_out = temp_buffer.data(); |
109 | 0 | jxl::msan::MemoryIsInitialized(next_in, avail_in); |
110 | 0 | if (!BrotliEncoderCompressStream(enc.get(), BROTLI_OPERATION_FINISH, |
111 | 0 | &avail_in, &next_in, &avail_out, &next_out, |
112 | 0 | &total_out)) { |
113 | 0 | return JXL_API_ERROR_NOSET("Brotli compression failed"); |
114 | 0 | } |
115 | 0 | size_t out_size = next_out - temp_buffer.data(); |
116 | 0 | jxl::msan::UnpoisonMemory(next_out - out_size, out_size); |
117 | 0 | out->resize(out->size() + out_size); |
118 | 0 | memcpy(out->data() + out->size() - out_size, temp_buffer.data(), out_size); |
119 | 0 | if (BrotliEncoderIsFinished(enc.get())) break; |
120 | 0 | } |
121 | | |
122 | 0 | return JXL_ENC_SUCCESS; |
123 | 0 | } |
124 | | |
125 | | // The JXL codestream can have level 5 or level 10. Levels have certain |
126 | | // restrictions such as max allowed image dimensions. This function checks the |
127 | | // level required to support the current encoder settings. The debug_string is |
128 | | // intended to be used for developer API error messages, and may be set to |
129 | | // nullptr. |
130 | 184 | int VerifyLevelSettings(const JxlEncoder* enc, std::string* debug_string) { |
131 | 184 | const auto& m = enc->metadata.m; |
132 | | |
133 | 184 | uint64_t xsize = enc->metadata.size.xsize(); |
134 | 184 | uint64_t ysize = enc->metadata.size.ysize(); |
135 | | // The uncompressed ICC size, if it is used. |
136 | 184 | size_t icc_size = 0; |
137 | 184 | if (m.color_encoding.WantICC()) { |
138 | 0 | icc_size = m.color_encoding.ICC().size(); |
139 | 0 | } |
140 | | |
141 | | // Level 10 checks |
142 | | |
143 | 184 | if (xsize > (1ull << 30ull) || ysize > (1ull << 30ull) || |
144 | 184 | xsize * ysize > (1ull << 40ull)) { |
145 | 0 | if (debug_string) *debug_string = "Too large image dimensions"; |
146 | 0 | return -1; |
147 | 0 | } |
148 | 184 | if (icc_size > (1ull << 28)) { |
149 | 0 | if (debug_string) *debug_string = "Too large ICC profile size"; |
150 | 0 | return -1; |
151 | 0 | } |
152 | 184 | if (m.num_extra_channels > 256) { |
153 | 0 | if (debug_string) *debug_string = "Too many extra channels"; |
154 | 0 | return -1; |
155 | 0 | } |
156 | | |
157 | | // Level 5 checks |
158 | | |
159 | 184 | if (!m.modular_16_bit_buffer_sufficient) { |
160 | 0 | if (debug_string) *debug_string = "Too high modular bit depth"; |
161 | 0 | return 10; |
162 | 0 | } |
163 | 184 | if (xsize > (1ull << 18ull) || ysize > (1ull << 18ull) || |
164 | 184 | xsize * ysize > (1ull << 28ull)) { |
165 | 0 | if (debug_string) *debug_string = "Too large image dimensions"; |
166 | 0 | return 10; |
167 | 0 | } |
168 | 184 | if (icc_size > (1ull << 22)) { |
169 | 0 | if (debug_string) *debug_string = "Too large ICC profile"; |
170 | 0 | return 10; |
171 | 0 | } |
172 | 184 | if (m.num_extra_channels > 4) { |
173 | 0 | if (debug_string) *debug_string = "Too many extra channels"; |
174 | 0 | return 10; |
175 | 0 | } |
176 | 184 | for (size_t i = 0; i < m.extra_channel_info.size(); ++i) { |
177 | 0 | if (m.extra_channel_info[i].type == jxl::ExtraChannel::kBlack) { |
178 | 0 | if (debug_string) *debug_string = "CMYK channel not allowed"; |
179 | 0 | return 10; |
180 | 0 | } |
181 | 0 | } |
182 | | |
183 | | // TODO(lode): also need to check if consecutive composite-still frames total |
184 | | // pixel amount doesn't exceed 2**28 in the case of level 5. This should be |
185 | | // done when adding frame and requires ability to add composite still frames |
186 | | // to be added first. |
187 | | |
188 | | // TODO(lode): also need to check animation duration of a frame. This should |
189 | | // be done when adding frame, but first requires implementing setting the |
190 | | // JxlFrameHeader for a frame. |
191 | | |
192 | | // TODO(lode): also need to check properties such as num_splines, num_patches, |
193 | | // modular_16bit_buffers and multiple properties of modular trees. However |
194 | | // these are not user-set properties so cannot be checked here, but decisions |
195 | | // the C++ encoder should be able to make based on the level. |
196 | | |
197 | | // All level 5 checks passes, so can return the more compatible level 5 |
198 | 184 | return 5; |
199 | 184 | } |
200 | | JxlEncoderStatus CheckValidBitdepth(uint32_t bits_per_sample, |
201 | 92 | uint32_t exponent_bits_per_sample) { |
202 | 92 | if (!exponent_bits_per_sample) { |
203 | | // The spec allows up to 31 for bits_per_sample here, but |
204 | | // the code does not (yet) support it. |
205 | 92 | if (!(bits_per_sample > 0 && bits_per_sample <= 24)) { |
206 | 0 | return JXL_API_ERROR_NOSET("Invalid value for bits_per_sample"); |
207 | 0 | } |
208 | 92 | } else if ((exponent_bits_per_sample > 8) || |
209 | 0 | (bits_per_sample > 24 + exponent_bits_per_sample) || |
210 | 0 | (bits_per_sample < 3 + exponent_bits_per_sample)) { |
211 | 0 | return JXL_API_ERROR_NOSET("Invalid float description"); |
212 | 0 | } |
213 | 92 | return JXL_ENC_SUCCESS; |
214 | 92 | } |
215 | | |
216 | | bool EncodeFrameIndexBox(const jxl::JxlEncoderFrameIndexBox& frame_index_box, |
217 | 0 | jxl::BitWriter& writer) { |
218 | 0 | bool ok = true; |
219 | 0 | int NF = 0; |
220 | 0 | for (size_t i = 0; i < frame_index_box.entries.size(); ++i) { |
221 | 0 | if (i == 0 || frame_index_box.entries[i].to_be_indexed) { |
222 | 0 | ++NF; |
223 | 0 | } |
224 | 0 | } |
225 | | // Frame index box contents varint + 8 bytes |
226 | | // continue with NF * 3 * varint |
227 | | // varint max length is 10 for 64 bit numbers, and these numbers |
228 | | // are limited to 63 bits. |
229 | 0 | static const int kVarintMaxLength = 10; |
230 | 0 | static const int kFrameIndexBoxHeaderLength = kVarintMaxLength + 8; |
231 | 0 | static const int kFrameIndexBoxElementLength = 3 * kVarintMaxLength; |
232 | 0 | const int buffer_size = |
233 | 0 | kFrameIndexBoxHeaderLength + NF * kFrameIndexBoxElementLength; |
234 | 0 | std::vector<uint8_t> buffer_vec(buffer_size); |
235 | 0 | uint8_t* buffer = buffer_vec.data(); |
236 | 0 | size_t output_pos = 0; |
237 | 0 | ok &= jxl::EncodeVarInt(NF, buffer_vec.size(), &output_pos, buffer); |
238 | 0 | StoreBE32(frame_index_box.TNUM, &buffer[output_pos]); |
239 | 0 | output_pos += 4; |
240 | 0 | StoreBE32(frame_index_box.TDEN, &buffer[output_pos]); |
241 | 0 | output_pos += 4; |
242 | | // When we record a frame in the index, the record needs to know |
243 | | // how many frames until the next indexed frame. That is why |
244 | | // we store the 'prev' record. That 'prev' record needs to store |
245 | | // the offset byte position to previously recorded indexed frame, |
246 | | // that's why we also trace previous to the previous frame. |
247 | 0 | int prev_prev_ix = -1; // For position offset (OFFi) delta coding. |
248 | 0 | int prev_ix = 0; |
249 | 0 | int T_prev = 0; |
250 | 0 | int T = 0; |
251 | 0 | for (size_t i = 1; i < frame_index_box.entries.size(); ++i) { |
252 | 0 | if (frame_index_box.entries[i].to_be_indexed) { |
253 | | // Now we can record the previous entry, since we need to store |
254 | | // there how many frames until the next one. |
255 | 0 | int64_t OFFi = frame_index_box.entries[prev_ix].OFFi; |
256 | 0 | if (prev_prev_ix != -1) { |
257 | | // Offi needs to be offset of start byte of this frame compared to start |
258 | | // byte of previous frame from this index in the JPEG XL codestream. For |
259 | | // the first frame, this is the offset from the first byte of the JPEG |
260 | | // XL codestream. |
261 | 0 | OFFi -= frame_index_box.entries[prev_prev_ix].OFFi; |
262 | 0 | } |
263 | 0 | int32_t Ti = T_prev; |
264 | 0 | int32_t Fi = i - prev_ix; |
265 | 0 | ok &= jxl::EncodeVarInt(OFFi, buffer_vec.size(), &output_pos, buffer); |
266 | 0 | ok &= jxl::EncodeVarInt(Ti, buffer_vec.size(), &output_pos, buffer); |
267 | 0 | ok &= jxl::EncodeVarInt(Fi, buffer_vec.size(), &output_pos, buffer); |
268 | 0 | prev_prev_ix = prev_ix; |
269 | 0 | prev_ix = i; |
270 | 0 | T_prev = T; |
271 | 0 | T += frame_index_box.entries[i].duration; |
272 | 0 | } |
273 | 0 | } |
274 | 0 | { |
275 | | // Last frame. |
276 | 0 | size_t i = frame_index_box.entries.size(); |
277 | 0 | int64_t OFFi = frame_index_box.entries[prev_ix].OFFi; |
278 | 0 | if (prev_prev_ix != -1) { |
279 | 0 | OFFi -= frame_index_box.entries[prev_prev_ix].OFFi; |
280 | 0 | } |
281 | 0 | int32_t Ti = T_prev; |
282 | 0 | int32_t Fi = i - prev_ix; |
283 | 0 | ok &= jxl::EncodeVarInt(OFFi, buffer_vec.size(), &output_pos, buffer); |
284 | 0 | ok &= jxl::EncodeVarInt(Ti, buffer_vec.size(), &output_pos, buffer); |
285 | 0 | ok &= jxl::EncodeVarInt(Fi, buffer_vec.size(), &output_pos, buffer); |
286 | 0 | } |
287 | | // Enough buffer has been allocated, this function should never fail in |
288 | | // writing. |
289 | 0 | JXL_ASSERT(ok); |
290 | 0 | return ok; |
291 | 0 | } |
292 | | |
293 | | } // namespace |
294 | | |
295 | 92 | JxlEncoderStatus JxlEncoderStruct::RefillOutputByteQueue() { |
296 | 92 | jxl::PaddedBytes bytes; |
297 | | |
298 | 92 | jxl::JxlEncoderQueuedInput& input = input_queue[0]; |
299 | | |
300 | | // TODO(lode): split this into 3 functions: for adding the signature and other |
301 | | // initial headers (jbrd, ...), one for adding frame, and one for adding user |
302 | | // box. |
303 | | |
304 | 92 | if (!wrote_bytes) { |
305 | | // First time encoding any data, verify the level 5 vs level 10 settings |
306 | 92 | std::string level_message; |
307 | 92 | int required_level = VerifyLevelSettings(this, &level_message); |
308 | | // Only level 5 and 10 are defined, and the function can return -1 to |
309 | | // indicate full incompatibility. |
310 | 92 | JXL_ASSERT(required_level == -1 || required_level == 5 || |
311 | 92 | required_level == 10); |
312 | | // codestream_level == -1 means auto-set to the required level |
313 | 92 | if (codestream_level == -1) codestream_level = required_level; |
314 | 92 | if (codestream_level == 5 && required_level != 5) { |
315 | | // If the required level is 10, return error rather than automatically |
316 | | // setting the level to 10, to avoid inadvertently creating a level 10 |
317 | | // JXL file while intending to target a level 5 decoder. |
318 | 0 | return JXL_API_ERROR( |
319 | 0 | this, JXL_ENC_ERR_API_USAGE, "%s", |
320 | 0 | ("Codestream level verification for level 5 failed: " + level_message) |
321 | 0 | .c_str()); |
322 | 0 | } |
323 | 92 | if (required_level == -1) { |
324 | 0 | return JXL_API_ERROR( |
325 | 0 | this, JXL_ENC_ERR_API_USAGE, "%s", |
326 | 0 | ("Codestream level verification for level 10 failed: " + |
327 | 0 | level_message) |
328 | 0 | .c_str()); |
329 | 0 | } |
330 | | |
331 | 92 | jxl::BitWriter writer; |
332 | 92 | if (!WriteHeaders(&metadata, &writer, nullptr)) { |
333 | 0 | return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, |
334 | 0 | "Failed to write codestream header"); |
335 | 0 | } |
336 | | // Only send ICC (at least several hundred bytes) if fields aren't enough. |
337 | 92 | if (metadata.m.color_encoding.WantICC()) { |
338 | 0 | if (!jxl::WriteICC(metadata.m.color_encoding.ICC(), &writer, |
339 | 0 | jxl::kLayerHeader, nullptr)) { |
340 | 0 | return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, |
341 | 0 | "Failed to write ICC profile"); |
342 | 0 | } |
343 | 0 | } |
344 | | // TODO(lode): preview should be added here if a preview image is added |
345 | | |
346 | 92 | writer.ZeroPadToByte(); |
347 | | |
348 | | // Not actually the end of frame, but the end of metadata/ICC, but helps |
349 | | // the next frame to start here for indexing purposes. |
350 | 92 | codestream_bytes_written_end_of_frame += |
351 | 92 | jxl::DivCeil(writer.BitsWritten(), 8); |
352 | | |
353 | 92 | bytes = std::move(writer).TakeBytes(); |
354 | | |
355 | 92 | if (MustUseContainer()) { |
356 | | // Add "JXL " and ftyp box. |
357 | 0 | output_byte_queue.insert( |
358 | 0 | output_byte_queue.end(), jxl::kContainerHeader, |
359 | 0 | jxl::kContainerHeader + sizeof(jxl::kContainerHeader)); |
360 | 0 | if (codestream_level != 5) { |
361 | | // Add jxll box directly after the ftyp box to indicate the codestream |
362 | | // level. |
363 | 0 | output_byte_queue.insert( |
364 | 0 | output_byte_queue.end(), jxl::kLevelBoxHeader, |
365 | 0 | jxl::kLevelBoxHeader + sizeof(jxl::kLevelBoxHeader)); |
366 | 0 | output_byte_queue.push_back(codestream_level); |
367 | 0 | } |
368 | | |
369 | | // Whether to write the basic info and color profile header of the |
370 | | // codestream into an early separate jxlp box, so that it comes before |
371 | | // metadata or jpeg reconstruction boxes. In theory this could simply |
372 | | // always be done, but there's no reason to add an extra box with box |
373 | | // header overhead if the codestream will already come immediately after |
374 | | // the signature and level boxes. |
375 | 0 | bool partial_header = store_jpeg_metadata || (use_boxes && !input.frame); |
376 | |
|
377 | 0 | if (partial_header) { |
378 | 0 | jxl::AppendBoxHeader(jxl::MakeBoxType("jxlp"), bytes.size() + 4, |
379 | 0 | /*unbounded=*/false, &output_byte_queue); |
380 | 0 | AppendJxlpBoxCounter(jxlp_counter++, /*last=*/false, |
381 | 0 | &output_byte_queue); |
382 | 0 | output_byte_queue.insert(output_byte_queue.end(), bytes.data(), |
383 | 0 | bytes.data() + bytes.size()); |
384 | 0 | bytes.clear(); |
385 | 0 | } |
386 | |
|
387 | 0 | if (store_jpeg_metadata && !jpeg_metadata.empty()) { |
388 | 0 | jxl::AppendBoxHeader(jxl::MakeBoxType("jbrd"), jpeg_metadata.size(), |
389 | 0 | false, &output_byte_queue); |
390 | 0 | output_byte_queue.insert(output_byte_queue.end(), jpeg_metadata.begin(), |
391 | 0 | jpeg_metadata.end()); |
392 | 0 | } |
393 | 0 | } |
394 | 92 | wrote_bytes = true; |
395 | 92 | } |
396 | | |
397 | | // Choose frame or box processing: exactly one of the two unique pointers (box |
398 | | // or frame) in the input queue item is non-null. |
399 | 92 | if (input.frame) { |
400 | 92 | jxl::MemoryManagerUniquePtr<jxl::JxlEncoderQueuedFrame> input_frame = |
401 | 92 | std::move(input.frame); |
402 | 92 | input_queue.erase(input_queue.begin()); |
403 | 92 | num_queued_frames--; |
404 | 92 | for (unsigned idx = 0; idx < input_frame->ec_initialized.size(); idx++) { |
405 | 0 | if (!input_frame->ec_initialized[idx]) { |
406 | 0 | return JXL_API_ERROR(this, JXL_ENC_ERR_API_USAGE, |
407 | 0 | "Extra channel %u is not initialized", idx); |
408 | 0 | } |
409 | 0 | } |
410 | | |
411 | | // TODO(zond): If the input queue is empty and the frames_closed is true, |
412 | | // then mark this frame as the last. |
413 | | |
414 | | // TODO(zond): Handle progressive mode like EncodeFile does it. |
415 | | // TODO(zond): Handle animation like EncodeFile does it, by checking if |
416 | | // JxlEncoderCloseFrames has been called and if the frame queue |
417 | | // is empty (to see if it's the last animation frame). |
418 | | |
419 | 92 | if (metadata.m.xyb_encoded) { |
420 | 92 | input_frame->option_values.cparams.color_transform = |
421 | 92 | jxl::ColorTransform::kXYB; |
422 | 92 | } else { |
423 | | // TODO(zond): Figure out when to use kYCbCr instead. |
424 | 0 | input_frame->option_values.cparams.color_transform = |
425 | 0 | jxl::ColorTransform::kNone; |
426 | 0 | } |
427 | | |
428 | 92 | jxl::BitWriter writer; |
429 | 92 | jxl::PassesEncoderState enc_state; |
430 | | |
431 | | // EncodeFrame creates jxl::FrameHeader object internally based on the |
432 | | // FrameInfo, imagebundle, cparams and metadata. Copy the information to |
433 | | // these. |
434 | 92 | jxl::ImageBundle& ib = input_frame->frame; |
435 | 92 | ib.name = input_frame->option_values.frame_name; |
436 | 92 | if (metadata.m.have_animation) { |
437 | 0 | ib.duration = input_frame->option_values.header.duration; |
438 | 0 | ib.timecode = input_frame->option_values.header.timecode; |
439 | 92 | } else { |
440 | | // If have_animation is false, the encoder should ignore the duration and |
441 | | // timecode values. However, assigning them to ib will cause the encoder |
442 | | // to write an invalid frame header that can't be decoded so ensure |
443 | | // they're the default value of 0 here. |
444 | 92 | ib.duration = 0; |
445 | 92 | ib.timecode = 0; |
446 | 92 | } |
447 | 92 | frame_index_box.AddFrame(codestream_bytes_written_end_of_frame, ib.duration, |
448 | 92 | input_frame->option_values.frame_index_box); |
449 | 92 | ib.blendmode = static_cast<jxl::BlendMode>( |
450 | 92 | input_frame->option_values.header.layer_info.blend_info.blendmode); |
451 | 92 | ib.blend = |
452 | 92 | input_frame->option_values.header.layer_info.blend_info.blendmode != |
453 | 92 | JXL_BLEND_REPLACE; |
454 | | |
455 | 92 | size_t save_as_reference = |
456 | 92 | input_frame->option_values.header.layer_info.save_as_reference; |
457 | 92 | ib.use_for_next_frame = !!save_as_reference; |
458 | | |
459 | 92 | jxl::FrameInfo frame_info; |
460 | 92 | bool last_frame = frames_closed && !num_queued_frames; |
461 | 92 | frame_info.is_last = last_frame; |
462 | 92 | frame_info.save_as_reference = save_as_reference; |
463 | 92 | frame_info.source = |
464 | 92 | input_frame->option_values.header.layer_info.blend_info.source; |
465 | 92 | frame_info.clamp = |
466 | 92 | input_frame->option_values.header.layer_info.blend_info.clamp; |
467 | 92 | frame_info.alpha_channel = |
468 | 92 | input_frame->option_values.header.layer_info.blend_info.alpha; |
469 | 92 | frame_info.extra_channel_blending_info.resize( |
470 | 92 | metadata.m.num_extra_channels); |
471 | | // If extra channel blend info has not been set, use the blend mode from the |
472 | | // layer_info. |
473 | 92 | JxlBlendInfo default_blend_info = |
474 | 92 | input_frame->option_values.header.layer_info.blend_info; |
475 | 92 | for (size_t i = 0; i < metadata.m.num_extra_channels; ++i) { |
476 | 0 | auto& to = frame_info.extra_channel_blending_info[i]; |
477 | 0 | const auto& from = |
478 | 0 | i < input_frame->option_values.extra_channel_blend_info.size() |
479 | 0 | ? input_frame->option_values.extra_channel_blend_info[i] |
480 | 0 | : default_blend_info; |
481 | 0 | to.mode = static_cast<jxl::BlendMode>(from.blendmode); |
482 | 0 | to.source = from.source; |
483 | 0 | to.alpha_channel = from.alpha; |
484 | 0 | to.clamp = (from.clamp != 0); |
485 | 0 | } |
486 | | |
487 | 92 | if (input_frame->option_values.header.layer_info.have_crop) { |
488 | 0 | ib.origin.x0 = input_frame->option_values.header.layer_info.crop_x0; |
489 | 0 | ib.origin.y0 = input_frame->option_values.header.layer_info.crop_y0; |
490 | 0 | } |
491 | 92 | JXL_ASSERT(writer.BitsWritten() == 0); |
492 | 92 | if (!jxl::EncodeFrame(input_frame->option_values.cparams, frame_info, |
493 | 92 | &metadata, input_frame->frame, &enc_state, cms, |
494 | 92 | thread_pool.get(), &writer, |
495 | 92 | /*aux_out=*/nullptr)) { |
496 | 0 | return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, "Failed to encode frame"); |
497 | 0 | } |
498 | 92 | codestream_bytes_written_beginning_of_frame = |
499 | 92 | codestream_bytes_written_end_of_frame; |
500 | 92 | codestream_bytes_written_end_of_frame += |
501 | 92 | jxl::DivCeil(writer.BitsWritten(), 8); |
502 | | |
503 | | // Possibly bytes already contains the codestream header: in case this is |
504 | | // the first frame, and the codestream header was not encoded as jxlp above. |
505 | 92 | bytes.append(std::move(writer).TakeBytes()); |
506 | 92 | if (MustUseContainer()) { |
507 | 0 | if (last_frame && jxlp_counter == 0) { |
508 | | // If this is the last frame and no jxlp boxes were used yet, it's |
509 | | // slighly more efficient to write a jxlc box since it has 4 bytes less |
510 | | // overhead. |
511 | 0 | jxl::AppendBoxHeader(jxl::MakeBoxType("jxlc"), bytes.size(), |
512 | 0 | /*unbounded=*/false, &output_byte_queue); |
513 | 0 | } else { |
514 | 0 | jxl::AppendBoxHeader(jxl::MakeBoxType("jxlp"), bytes.size() + 4, |
515 | 0 | /*unbounded=*/false, &output_byte_queue); |
516 | 0 | AppendJxlpBoxCounter(jxlp_counter++, last_frame, &output_byte_queue); |
517 | 0 | } |
518 | 0 | } |
519 | | |
520 | 92 | output_byte_queue.insert(output_byte_queue.end(), bytes.data(), |
521 | 92 | bytes.data() + bytes.size()); |
522 | | |
523 | 92 | last_used_cparams = input_frame->option_values.cparams; |
524 | 92 | if (last_frame && frame_index_box.StoreFrameIndexBox()) { |
525 | 0 | bytes.clear(); |
526 | 0 | EncodeFrameIndexBox(frame_index_box, writer); |
527 | 0 | jxl::AppendBoxHeader(jxl::MakeBoxType("jxli"), bytes.size(), |
528 | 0 | /*unbounded=*/false, &output_byte_queue); |
529 | 0 | } |
530 | 92 | } else { |
531 | | // Not a frame, so is a box instead |
532 | 0 | jxl::MemoryManagerUniquePtr<jxl::JxlEncoderQueuedBox> box = |
533 | 0 | std::move(input.box); |
534 | 0 | input_queue.erase(input_queue.begin()); |
535 | 0 | num_queued_boxes--; |
536 | |
|
537 | 0 | if (box->compress_box) { |
538 | 0 | jxl::PaddedBytes compressed(4); |
539 | | // Prepend the original box type in the brob box contents |
540 | 0 | for (size_t i = 0; i < 4; i++) { |
541 | 0 | compressed[i] = static_cast<uint8_t>(box->type[i]); |
542 | 0 | } |
543 | 0 | if (JXL_ENC_SUCCESS != |
544 | 0 | BrotliCompress((brotli_effort >= 0 ? brotli_effort : 4), |
545 | 0 | box->contents.data(), box->contents.size(), |
546 | 0 | &compressed)) { |
547 | 0 | return JXL_API_ERROR(this, JXL_ENC_ERR_GENERIC, |
548 | 0 | "Brotli compression for brob box failed"); |
549 | 0 | } |
550 | 0 | jxl::AppendBoxHeader(jxl::MakeBoxType("brob"), compressed.size(), false, |
551 | 0 | &output_byte_queue); |
552 | 0 | output_byte_queue.insert(output_byte_queue.end(), compressed.data(), |
553 | 0 | compressed.data() + compressed.size()); |
554 | 0 | } else { |
555 | 0 | jxl::AppendBoxHeader(box->type, box->contents.size(), false, |
556 | 0 | &output_byte_queue); |
557 | 0 | output_byte_queue.insert(output_byte_queue.end(), box->contents.data(), |
558 | 0 | box->contents.data() + box->contents.size()); |
559 | 0 | } |
560 | 0 | } |
561 | | |
562 | 92 | return JXL_ENC_SUCCESS; |
563 | 92 | } |
564 | | |
565 | | JxlEncoderStatus JxlEncoderSetColorEncoding(JxlEncoder* enc, |
566 | 92 | const JxlColorEncoding* color) { |
567 | 92 | if (!enc->basic_info_set) { |
568 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Basic info not yet set"); |
569 | 0 | } |
570 | 92 | if (enc->color_encoding_set) { |
571 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
572 | 0 | "Color encoding is already set"); |
573 | 0 | } |
574 | 92 | if (!jxl::ConvertExternalToInternalColorEncoding( |
575 | 92 | *color, &enc->metadata.m.color_encoding)) { |
576 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_GENERIC, "Error in color conversion"); |
577 | 0 | } |
578 | 92 | if (enc->metadata.m.color_encoding.GetColorSpace() == |
579 | 92 | jxl::ColorSpace::kGray) { |
580 | 0 | if (enc->basic_info.num_color_channels != 1) |
581 | 0 | return JXL_API_ERROR( |
582 | 0 | enc, JXL_ENC_ERR_API_USAGE, |
583 | 0 | "Cannot use grayscale color encoding with num_color_channels != 1"); |
584 | 92 | } else { |
585 | 92 | if (enc->basic_info.num_color_channels != 3) |
586 | 0 | return JXL_API_ERROR( |
587 | 92 | enc, JXL_ENC_ERR_API_USAGE, |
588 | 92 | "Cannot use RGB color encoding with num_color_channels != 3"); |
589 | 92 | } |
590 | 92 | enc->color_encoding_set = true; |
591 | 92 | if (!enc->intensity_target_set) { |
592 | 0 | jxl::SetIntensityTarget(&enc->metadata.m); |
593 | 0 | } |
594 | 92 | return JXL_ENC_SUCCESS; |
595 | 92 | } |
596 | | |
597 | | JxlEncoderStatus JxlEncoderSetICCProfile(JxlEncoder* enc, |
598 | | const uint8_t* icc_profile, |
599 | 0 | size_t size) { |
600 | 0 | if (!enc->basic_info_set) { |
601 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Basic info not yet set"); |
602 | 0 | } |
603 | 0 | if (enc->color_encoding_set) { |
604 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
605 | 0 | "ICC profile is already set"); |
606 | 0 | } |
607 | 0 | jxl::PaddedBytes icc; |
608 | 0 | icc.assign(icc_profile, icc_profile + size); |
609 | 0 | if (!enc->metadata.m.color_encoding.SetICC(std::move(icc))) { |
610 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_BAD_INPUT, |
611 | 0 | "ICC profile could not be set"); |
612 | 0 | } |
613 | 0 | if (enc->metadata.m.color_encoding.GetColorSpace() == |
614 | 0 | jxl::ColorSpace::kGray) { |
615 | 0 | if (enc->basic_info.num_color_channels != 1) |
616 | 0 | return JXL_API_ERROR( |
617 | 0 | enc, JXL_ENC_ERR_BAD_INPUT, |
618 | 0 | "Cannot use grayscale ICC profile with num_color_channels != 1"); |
619 | 0 | } else { |
620 | 0 | if (enc->basic_info.num_color_channels != 3) |
621 | 0 | return JXL_API_ERROR( |
622 | 0 | enc, JXL_ENC_ERR_BAD_INPUT, |
623 | 0 | "Cannot use RGB ICC profile with num_color_channels != 3"); |
624 | | // TODO(jon): also check that a kBlack extra channel is provided in the CMYK |
625 | | // case |
626 | 0 | } |
627 | 0 | enc->color_encoding_set = true; |
628 | 0 | if (!enc->intensity_target_set) { |
629 | 0 | jxl::SetIntensityTarget(&enc->metadata.m); |
630 | 0 | } |
631 | |
|
632 | 0 | if (!enc->basic_info.uses_original_profile) { |
633 | 0 | enc->metadata.m.color_encoding.DecideIfWantICC(); |
634 | 0 | } |
635 | |
|
636 | 0 | return JXL_ENC_SUCCESS; |
637 | 0 | } |
638 | | |
639 | 230 | void JxlEncoderInitBasicInfo(JxlBasicInfo* info) { |
640 | 230 | info->have_container = JXL_FALSE; |
641 | 230 | info->xsize = 0; |
642 | 230 | info->ysize = 0; |
643 | 230 | info->bits_per_sample = 8; |
644 | 230 | info->exponent_bits_per_sample = 0; |
645 | 230 | info->intensity_target = 0.f; |
646 | 230 | info->min_nits = 0.f; |
647 | 230 | info->relative_to_max_display = JXL_FALSE; |
648 | 230 | info->linear_below = 0.f; |
649 | 230 | info->uses_original_profile = JXL_FALSE; |
650 | 230 | info->have_preview = JXL_FALSE; |
651 | 230 | info->have_animation = JXL_FALSE; |
652 | 230 | info->orientation = JXL_ORIENT_IDENTITY; |
653 | 230 | info->num_color_channels = 3; |
654 | 230 | info->num_extra_channels = 0; |
655 | 230 | info->alpha_bits = 0; |
656 | 230 | info->alpha_exponent_bits = 0; |
657 | 230 | info->alpha_premultiplied = JXL_FALSE; |
658 | 230 | info->preview.xsize = 0; |
659 | 230 | info->preview.ysize = 0; |
660 | 230 | info->intrinsic_xsize = 0; |
661 | 230 | info->intrinsic_ysize = 0; |
662 | 230 | info->animation.tps_numerator = 10; |
663 | 230 | info->animation.tps_denominator = 1; |
664 | 230 | info->animation.num_loops = 0; |
665 | 230 | info->animation.have_timecodes = JXL_FALSE; |
666 | 230 | } |
667 | | |
668 | 92 | void JxlEncoderInitFrameHeader(JxlFrameHeader* frame_header) { |
669 | | // For each field, the default value of the specification is used. Depending |
670 | | // on wheter an animation frame, or a composite still blending frame, is used, |
671 | | // different fields have to be set up by the user after initing the frame |
672 | | // header. |
673 | 92 | frame_header->duration = 0; |
674 | 92 | frame_header->timecode = 0; |
675 | 92 | frame_header->name_length = 0; |
676 | | // In the specification, the default value of is_last is !frame_type, and the |
677 | | // default frame_type is kRegularFrame which has value 0, so is_last is true |
678 | | // by default. However, the encoder does not use this value (the field exists |
679 | | // for the decoder to set) since last frame is determined by usage of |
680 | | // JxlEncoderCloseFrames instead. |
681 | 92 | frame_header->is_last = JXL_TRUE; |
682 | 92 | frame_header->layer_info.have_crop = JXL_FALSE; |
683 | 92 | frame_header->layer_info.crop_x0 = 0; |
684 | 92 | frame_header->layer_info.crop_y0 = 0; |
685 | | // These must be set if have_crop is enabled, but the default value has |
686 | | // have_crop false, and these dimensions 0. The user must set these to the |
687 | | // desired size after enabling have_crop (which is not yet implemented). |
688 | 92 | frame_header->layer_info.xsize = 0; |
689 | 92 | frame_header->layer_info.ysize = 0; |
690 | 92 | JxlEncoderInitBlendInfo(&frame_header->layer_info.blend_info); |
691 | 92 | frame_header->layer_info.save_as_reference = 0; |
692 | 92 | } |
693 | | |
694 | 92 | void JxlEncoderInitBlendInfo(JxlBlendInfo* blend_info) { |
695 | | // Default blend mode in the specification is 0. Note that combining |
696 | | // blend mode of replace with a duration is not useful, but the user has to |
697 | | // manually set duration in case of animation, or manually change the blend |
698 | | // mode in case of composite stills, so initing to a combination that is not |
699 | | // useful on its own is not an issue. |
700 | 92 | blend_info->blendmode = JXL_BLEND_REPLACE; |
701 | 92 | blend_info->source = 0; |
702 | 92 | blend_info->alpha = 0; |
703 | 92 | blend_info->clamp = 0; |
704 | 92 | } |
705 | | |
706 | | JxlEncoderStatus JxlEncoderSetBasicInfo(JxlEncoder* enc, |
707 | 92 | const JxlBasicInfo* info) { |
708 | 92 | if (!enc->metadata.size.Set(info->xsize, info->ysize)) { |
709 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Invalid dimensions"); |
710 | 0 | } |
711 | 92 | if (JXL_ENC_SUCCESS != CheckValidBitdepth(info->bits_per_sample, |
712 | 92 | info->exponent_bits_per_sample)) { |
713 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Invalid bit depth"); |
714 | 0 | } |
715 | 92 | enc->metadata.m.bit_depth.bits_per_sample = info->bits_per_sample; |
716 | 92 | enc->metadata.m.bit_depth.exponent_bits_per_sample = |
717 | 92 | info->exponent_bits_per_sample; |
718 | 92 | enc->metadata.m.bit_depth.floating_point_sample = |
719 | 92 | (info->exponent_bits_per_sample != 0u); |
720 | 92 | enc->metadata.m.modular_16_bit_buffer_sufficient = |
721 | 92 | (!info->uses_original_profile || info->bits_per_sample <= 12) && |
722 | 92 | info->alpha_bits <= 12; |
723 | | |
724 | | // The number of extra channels includes the alpha channel, so for example and |
725 | | // RGBA with no other extra channels, has exactly num_extra_channels == 1 |
726 | 92 | enc->metadata.m.num_extra_channels = info->num_extra_channels; |
727 | 92 | enc->metadata.m.extra_channel_info.resize(enc->metadata.m.num_extra_channels); |
728 | 92 | if (info->num_extra_channels == 0 && info->alpha_bits) { |
729 | 0 | return JXL_API_ERROR( |
730 | 0 | enc, JXL_ENC_ERR_API_USAGE, |
731 | 0 | "when alpha_bits is non-zero, the number of channels must be at least " |
732 | 0 | "1"); |
733 | 0 | } |
734 | | // If the user provides non-zero alpha_bits, we make the channel info at index |
735 | | // zero the appropriate alpha channel. |
736 | 92 | if (info->alpha_bits) { |
737 | 0 | JxlExtraChannelInfo channel_info; |
738 | 0 | JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &channel_info); |
739 | 0 | channel_info.bits_per_sample = info->alpha_bits; |
740 | 0 | channel_info.exponent_bits_per_sample = info->alpha_exponent_bits; |
741 | 0 | if (JxlEncoderSetExtraChannelInfo(enc, 0, &channel_info)) { |
742 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
743 | 0 | "Problem setting extra channel info for alpha"); |
744 | 0 | } |
745 | 0 | } |
746 | | |
747 | 92 | enc->metadata.m.xyb_encoded = !info->uses_original_profile; |
748 | 92 | if (info->orientation > 0 && info->orientation <= 8) { |
749 | 92 | enc->metadata.m.orientation = info->orientation; |
750 | 92 | } else { |
751 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
752 | 0 | "Invalid value for orientation field"); |
753 | 0 | } |
754 | 92 | if (info->num_color_channels != 1 && info->num_color_channels != 3) { |
755 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
756 | 0 | "Invalid number of color channels"); |
757 | 0 | } |
758 | 92 | if (info->intensity_target != 0) { |
759 | 0 | enc->metadata.m.SetIntensityTarget(info->intensity_target); |
760 | 0 | enc->intensity_target_set = true; |
761 | 92 | } else if (enc->color_encoding_set || enc->metadata.m.xyb_encoded) { |
762 | | // If both conditions are false, JxlEncoderSetColorEncoding will be called |
763 | | // later and we will get one more chance to call jxl::SetIntensityTarget, |
764 | | // after the color encoding is indeed set. |
765 | 92 | jxl::SetIntensityTarget(&enc->metadata.m); |
766 | 92 | enc->intensity_target_set = true; |
767 | 92 | } |
768 | 92 | enc->metadata.m.tone_mapping.min_nits = info->min_nits; |
769 | 92 | enc->metadata.m.tone_mapping.relative_to_max_display = |
770 | 92 | info->relative_to_max_display; |
771 | 92 | enc->metadata.m.tone_mapping.linear_below = info->linear_below; |
772 | 92 | enc->basic_info = *info; |
773 | 92 | enc->basic_info_set = true; |
774 | | |
775 | 92 | enc->metadata.m.have_animation = info->have_animation; |
776 | 92 | if (info->have_animation) { |
777 | 0 | if (info->animation.tps_denominator < 1) { |
778 | 0 | return JXL_API_ERROR( |
779 | 0 | enc, JXL_ENC_ERR_API_USAGE, |
780 | 0 | "If animation is used, tps_denominator must be >= 1"); |
781 | 0 | } |
782 | 0 | if (info->animation.tps_numerator < 1) { |
783 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
784 | 0 | "If animation is used, tps_numerator must be >= 1"); |
785 | 0 | } |
786 | 0 | enc->metadata.m.animation.tps_numerator = info->animation.tps_numerator; |
787 | 0 | enc->metadata.m.animation.tps_denominator = info->animation.tps_denominator; |
788 | 0 | enc->metadata.m.animation.num_loops = info->animation.num_loops; |
789 | 0 | enc->metadata.m.animation.have_timecodes = info->animation.have_timecodes; |
790 | 0 | } |
791 | 92 | std::string level_message; |
792 | 92 | int required_level = VerifyLevelSettings(enc, &level_message); |
793 | 92 | if (required_level == -1 || |
794 | 92 | (static_cast<int>(enc->codestream_level) < required_level && |
795 | 92 | enc->codestream_level != -1)) { |
796 | 0 | return JXL_API_ERROR( |
797 | 0 | enc, JXL_ENC_ERR_API_USAGE, "%s", |
798 | 0 | ("Codestream level verification for level " + |
799 | 0 | std::to_string(enc->codestream_level) + " failed: " + level_message) |
800 | 0 | .c_str()); |
801 | 0 | } |
802 | 92 | return JXL_ENC_SUCCESS; |
803 | 92 | } |
804 | | |
805 | | void JxlEncoderInitExtraChannelInfo(JxlExtraChannelType type, |
806 | 0 | JxlExtraChannelInfo* info) { |
807 | 0 | info->type = type; |
808 | 0 | info->bits_per_sample = 8; |
809 | 0 | info->exponent_bits_per_sample = 0; |
810 | 0 | info->dim_shift = 0; |
811 | 0 | info->name_length = 0; |
812 | 0 | info->alpha_premultiplied = JXL_FALSE; |
813 | 0 | info->spot_color[0] = 0; |
814 | 0 | info->spot_color[1] = 0; |
815 | 0 | info->spot_color[2] = 0; |
816 | 0 | info->spot_color[3] = 0; |
817 | 0 | info->cfa_channel = 0; |
818 | 0 | } |
819 | | |
820 | | JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelInfo( |
821 | 0 | JxlEncoder* enc, size_t index, const JxlExtraChannelInfo* info) { |
822 | 0 | if (index >= enc->metadata.m.num_extra_channels) { |
823 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
824 | 0 | "Invalid value for the index of extra channel"); |
825 | 0 | } |
826 | 0 | if (JXL_ENC_SUCCESS != CheckValidBitdepth(info->bits_per_sample, |
827 | 0 | info->exponent_bits_per_sample)) { |
828 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, "Invalid bit depth"); |
829 | 0 | } |
830 | | |
831 | 0 | jxl::ExtraChannelInfo& channel = enc->metadata.m.extra_channel_info[index]; |
832 | 0 | channel.type = static_cast<jxl::ExtraChannel>(info->type); |
833 | 0 | channel.bit_depth.bits_per_sample = info->bits_per_sample; |
834 | 0 | enc->metadata.m.modular_16_bit_buffer_sufficient &= |
835 | 0 | info->bits_per_sample <= 12; |
836 | 0 | channel.bit_depth.exponent_bits_per_sample = info->exponent_bits_per_sample; |
837 | 0 | channel.bit_depth.floating_point_sample = info->exponent_bits_per_sample != 0; |
838 | 0 | channel.dim_shift = info->dim_shift; |
839 | 0 | channel.name = ""; |
840 | 0 | channel.alpha_associated = (info->alpha_premultiplied != 0); |
841 | 0 | channel.cfa_channel = info->cfa_channel; |
842 | 0 | channel.spot_color[0] = info->spot_color[0]; |
843 | 0 | channel.spot_color[1] = info->spot_color[1]; |
844 | 0 | channel.spot_color[2] = info->spot_color[2]; |
845 | 0 | channel.spot_color[3] = info->spot_color[3]; |
846 | 0 | std::string level_message; |
847 | 0 | int required_level = VerifyLevelSettings(enc, &level_message); |
848 | 0 | if (required_level == -1 || |
849 | 0 | (static_cast<int>(enc->codestream_level) < required_level && |
850 | 0 | enc->codestream_level != -1)) { |
851 | 0 | return JXL_API_ERROR( |
852 | 0 | enc, JXL_ENC_ERR_API_USAGE, "%s", |
853 | 0 | ("Codestream level verification for level " + |
854 | 0 | std::to_string(enc->codestream_level) + " failed: " + level_message) |
855 | 0 | .c_str()); |
856 | 0 | } |
857 | 0 | return JXL_ENC_SUCCESS; |
858 | 0 | } |
859 | | |
860 | | JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelName(JxlEncoder* enc, |
861 | | size_t index, |
862 | | const char* name, |
863 | 0 | size_t size) { |
864 | 0 | if (index >= enc->metadata.m.num_extra_channels) { |
865 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
866 | 0 | "Invalid value for the index of extra channel"); |
867 | 0 | } |
868 | 0 | enc->metadata.m.extra_channel_info[index].name = |
869 | 0 | std::string(name, name + size); |
870 | 0 | return JXL_ENC_SUCCESS; |
871 | 0 | } |
872 | | |
873 | | JxlEncoderFrameSettings* JxlEncoderFrameSettingsCreate( |
874 | 92 | JxlEncoder* enc, const JxlEncoderFrameSettings* source) { |
875 | 92 | auto opts = jxl::MemoryManagerMakeUnique<JxlEncoderFrameSettings>( |
876 | 92 | &enc->memory_manager); |
877 | 92 | if (!opts) return nullptr; |
878 | 92 | opts->enc = enc; |
879 | 92 | if (source != nullptr) { |
880 | 0 | opts->values = source->values; |
881 | 92 | } else { |
882 | 92 | opts->values.lossless = false; |
883 | 92 | } |
884 | 92 | opts->values.cparams.level = enc->codestream_level; |
885 | 92 | JxlEncoderFrameSettings* ret = opts.get(); |
886 | 92 | enc->encoder_options.emplace_back(std::move(opts)); |
887 | 92 | return ret; |
888 | 92 | } |
889 | | |
890 | | JxlEncoderFrameSettings* JxlEncoderOptionsCreate( |
891 | 0 | JxlEncoder* enc, const JxlEncoderFrameSettings* source) { |
892 | | // Deprecated function name, call the non-deprecated function |
893 | 0 | return JxlEncoderFrameSettingsCreate(enc, source); |
894 | 0 | } |
895 | | |
896 | | JxlEncoderStatus JxlEncoderSetFrameLossless( |
897 | 0 | JxlEncoderFrameSettings* frame_settings, const JXL_BOOL lossless) { |
898 | 0 | if (lossless && frame_settings->enc->basic_info_set && |
899 | 0 | frame_settings->enc->metadata.m.xyb_encoded) { |
900 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
901 | 0 | "Set use_original_profile=true for lossless encoding"); |
902 | 0 | } |
903 | 0 | frame_settings->values.lossless = lossless; |
904 | 0 | return JXL_ENC_SUCCESS; |
905 | 0 | } |
906 | | |
907 | | JxlEncoderStatus JxlEncoderOptionsSetLossless( |
908 | 0 | JxlEncoderFrameSettings* frame_settings, JXL_BOOL lossless) { |
909 | | // Deprecated function name, call the non-deprecated function |
910 | 0 | return JxlEncoderSetFrameLossless(frame_settings, lossless); |
911 | 0 | } |
912 | | |
913 | | JxlEncoderStatus JxlEncoderOptionsSetEffort( |
914 | 0 | JxlEncoderFrameSettings* frame_settings, const int effort) { |
915 | 0 | return JxlEncoderFrameSettingsSetOption(frame_settings, |
916 | 0 | JXL_ENC_FRAME_SETTING_EFFORT, effort); |
917 | 0 | } |
918 | | |
919 | | JxlEncoderStatus JxlEncoderSetFrameDistance( |
920 | 0 | JxlEncoderFrameSettings* frame_settings, float distance) { |
921 | 0 | if (distance < 0.f || distance > 25.f) { |
922 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
923 | 0 | "Distance has to be in [0.0..25.0]"); |
924 | 0 | } |
925 | 0 | if (distance > 0.f && distance < 0.01f) { |
926 | 0 | distance = 0.01f; |
927 | 0 | } |
928 | 0 | frame_settings->values.cparams.butteraugli_distance = distance; |
929 | 0 | return JXL_ENC_SUCCESS; |
930 | 0 | } |
931 | | |
932 | | JxlEncoderStatus JxlEncoderOptionsSetDistance( |
933 | 0 | JxlEncoderFrameSettings* frame_settings, float distance) { |
934 | | // Deprecated function name, call the non-deprecated function |
935 | 0 | return JxlEncoderSetFrameDistance(frame_settings, distance); |
936 | 0 | } |
937 | | |
938 | | JxlEncoderStatus JxlEncoderOptionsSetDecodingSpeed( |
939 | 0 | JxlEncoderFrameSettings* frame_settings, int tier) { |
940 | 0 | return JxlEncoderFrameSettingsSetOption( |
941 | 0 | frame_settings, JXL_ENC_FRAME_SETTING_DECODING_SPEED, tier); |
942 | 0 | } |
943 | | |
944 | | JxlEncoderStatus JxlEncoderFrameSettingsSetOption( |
945 | | JxlEncoderFrameSettings* frame_settings, JxlEncoderFrameSettingId option, |
946 | 0 | int64_t value) { |
947 | | // check if value is -1, 0 or 1 for Override-type options |
948 | 0 | switch (option) { |
949 | 0 | case JXL_ENC_FRAME_SETTING_NOISE: |
950 | 0 | case JXL_ENC_FRAME_SETTING_DOTS: |
951 | 0 | case JXL_ENC_FRAME_SETTING_PATCHES: |
952 | 0 | case JXL_ENC_FRAME_SETTING_GABORISH: |
953 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR: |
954 | 0 | case JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE: |
955 | 0 | case JXL_ENC_FRAME_SETTING_GROUP_ORDER: |
956 | 0 | case JXL_ENC_FRAME_SETTING_RESPONSIVE: |
957 | 0 | case JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC: |
958 | 0 | case JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC: |
959 | 0 | case JXL_ENC_FRAME_SETTING_LOSSY_PALETTE: |
960 | 0 | case JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL: |
961 | 0 | if (value < -1 || value > 1) { |
962 | 0 | return JXL_API_ERROR( |
963 | 0 | frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
964 | 0 | "Option value has to be -1 (default), 0 (off) or 1 (on)"); |
965 | 0 | } |
966 | 0 | break; |
967 | 0 | default: |
968 | 0 | break; |
969 | 0 | } |
970 | | |
971 | 0 | switch (option) { |
972 | 0 | case JXL_ENC_FRAME_SETTING_EFFORT: |
973 | 0 | if (value < 1 || value > 9) { |
974 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, |
975 | 0 | "Encode effort has to be in [1..9]"); |
976 | 0 | } |
977 | 0 | frame_settings->values.cparams.speed_tier = |
978 | 0 | static_cast<jxl::SpeedTier>(10 - value); |
979 | 0 | return JXL_ENC_SUCCESS; |
980 | 0 | case JXL_ENC_FRAME_SETTING_BROTLI_EFFORT: |
981 | 0 | if (value < -1 || value > 11) { |
982 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
983 | 0 | "Brotli effort has to be in [-1..11]"); |
984 | 0 | } |
985 | | // set cparams for brotli use in JPEG frames |
986 | 0 | frame_settings->values.cparams.brotli_effort = value; |
987 | | // set enc option for brotli use in brob boxes |
988 | 0 | frame_settings->enc->brotli_effort = value; |
989 | 0 | return JXL_ENC_SUCCESS; |
990 | 0 | case JXL_ENC_FRAME_SETTING_DECODING_SPEED: |
991 | 0 | if (value < 0 || value > 4) { |
992 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, |
993 | 0 | "Decoding speed has to be in [0..4]"); |
994 | 0 | } |
995 | 0 | frame_settings->values.cparams.decoding_speed_tier = value; |
996 | 0 | return JXL_ENC_SUCCESS; |
997 | 0 | case JXL_ENC_FRAME_SETTING_RESAMPLING: |
998 | 0 | if (value != -1 && value != 1 && value != 2 && value != 4 && value != 8) { |
999 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1000 | 0 | "Resampling factor has to be 1, 2, 4 or 8"); |
1001 | 0 | } |
1002 | 0 | frame_settings->values.cparams.resampling = value; |
1003 | 0 | return JXL_ENC_SUCCESS; |
1004 | 0 | case JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING: |
1005 | | // TOOD(lode): the jxl codestream allows choosing a different resampling |
1006 | | // factor for each extra channel, independently per frame. Move this |
1007 | | // option to a JxlEncoderFrameSettings-option that can be set per extra |
1008 | | // channel, so needs its own function rather than |
1009 | | // JxlEncoderFrameSettingsSetOption due to the extra channel index |
1010 | | // argument required. |
1011 | 0 | if (value != -1 && value != 1 && value != 2 && value != 4 && value != 8) { |
1012 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1013 | 0 | "Resampling factor has to be 1, 2, 4 or 8"); |
1014 | 0 | } |
1015 | 0 | frame_settings->values.cparams.ec_resampling = value; |
1016 | 0 | return JXL_ENC_SUCCESS; |
1017 | 0 | case JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED: |
1018 | 0 | if (value < 0 || value > 1) { |
1019 | 0 | return JXL_ENC_ERROR; |
1020 | 0 | } |
1021 | 0 | frame_settings->values.cparams.already_downsampled = (value == 1); |
1022 | 0 | return JXL_ENC_SUCCESS; |
1023 | 0 | case JXL_ENC_FRAME_SETTING_NOISE: |
1024 | 0 | frame_settings->values.cparams.noise = static_cast<jxl::Override>(value); |
1025 | 0 | return JXL_ENC_SUCCESS; |
1026 | 0 | case JXL_ENC_FRAME_SETTING_DOTS: |
1027 | 0 | frame_settings->values.cparams.dots = static_cast<jxl::Override>(value); |
1028 | 0 | return JXL_ENC_SUCCESS; |
1029 | 0 | case JXL_ENC_FRAME_SETTING_PATCHES: |
1030 | 0 | frame_settings->values.cparams.patches = |
1031 | 0 | static_cast<jxl::Override>(value); |
1032 | 0 | return JXL_ENC_SUCCESS; |
1033 | 0 | case JXL_ENC_FRAME_SETTING_EPF: |
1034 | 0 | if (value < -1 || value > 3) { |
1035 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1036 | 0 | "EPF value has to be in [-1..3]"); |
1037 | 0 | } |
1038 | 0 | frame_settings->values.cparams.epf = static_cast<int>(value); |
1039 | 0 | return JXL_ENC_SUCCESS; |
1040 | 0 | case JXL_ENC_FRAME_SETTING_GABORISH: |
1041 | 0 | frame_settings->values.cparams.gaborish = |
1042 | 0 | static_cast<jxl::Override>(value); |
1043 | 0 | return JXL_ENC_SUCCESS; |
1044 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR: |
1045 | 0 | frame_settings->values.cparams.modular_mode = (value == 1); |
1046 | 0 | return JXL_ENC_SUCCESS; |
1047 | 0 | case JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE: |
1048 | 0 | frame_settings->values.cparams.keep_invisible = |
1049 | 0 | static_cast<jxl::Override>(value); |
1050 | 0 | return JXL_ENC_SUCCESS; |
1051 | 0 | case JXL_ENC_FRAME_SETTING_GROUP_ORDER: |
1052 | 0 | frame_settings->values.cparams.centerfirst = (value == 1); |
1053 | 0 | return JXL_ENC_SUCCESS; |
1054 | 0 | case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X: |
1055 | 0 | if (value < -1) { |
1056 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1057 | 0 | "Center x coordinate has to be -1 or positive"); |
1058 | 0 | } |
1059 | 0 | frame_settings->values.cparams.center_x = static_cast<size_t>(value); |
1060 | 0 | return JXL_ENC_SUCCESS; |
1061 | 0 | case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y: |
1062 | 0 | if (value < -1) { |
1063 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1064 | 0 | "Center y coordinate has to be -1 or positive"); |
1065 | 0 | } |
1066 | 0 | frame_settings->values.cparams.center_y = static_cast<size_t>(value); |
1067 | 0 | return JXL_ENC_SUCCESS; |
1068 | 0 | case JXL_ENC_FRAME_SETTING_RESPONSIVE: |
1069 | 0 | frame_settings->values.cparams.responsive = value; |
1070 | 0 | return JXL_ENC_SUCCESS; |
1071 | 0 | case JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC: |
1072 | 0 | frame_settings->values.cparams.progressive_mode = value; |
1073 | 0 | return JXL_ENC_SUCCESS; |
1074 | 0 | case JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC: |
1075 | 0 | frame_settings->values.cparams.qprogressive_mode = value; |
1076 | 0 | return JXL_ENC_SUCCESS; |
1077 | 0 | case JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC: |
1078 | 0 | if (value < -1 || value > 2) { |
1079 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1080 | 0 | "Progressive DC has to be in [-1..2]"); |
1081 | 0 | } |
1082 | 0 | frame_settings->values.cparams.progressive_dc = value; |
1083 | 0 | return JXL_ENC_SUCCESS; |
1084 | 0 | case JXL_ENC_FRAME_SETTING_PALETTE_COLORS: |
1085 | 0 | if (value < -1 || value > 70913) { |
1086 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1087 | 0 | "Option value has to be in [-1..70913]"); |
1088 | 0 | } |
1089 | 0 | if (value == -1) { |
1090 | 0 | frame_settings->values.cparams.palette_colors = 1 << 10; |
1091 | 0 | } else { |
1092 | 0 | frame_settings->values.cparams.palette_colors = value; |
1093 | 0 | } |
1094 | 0 | return JXL_ENC_SUCCESS; |
1095 | 0 | case JXL_ENC_FRAME_SETTING_LOSSY_PALETTE: |
1096 | | // TODO(lode): the defaults of some palette settings depend on others. |
1097 | | // See the logic in cjxl. Similar for other settings. This should be |
1098 | | // handled in the encoder during JxlEncoderProcessOutput (or, |
1099 | | // alternatively, in the cjxl binary like now) |
1100 | 0 | frame_settings->values.cparams.lossy_palette = (value == 1); |
1101 | 0 | return JXL_ENC_SUCCESS; |
1102 | 0 | case JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM: |
1103 | 0 | if (value < -1 || value > 2) { |
1104 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1105 | 0 | "Option value has to be in [-1..2]"); |
1106 | 0 | } |
1107 | 0 | if (value == -1) { |
1108 | 0 | frame_settings->values.cparams.color_transform = |
1109 | 0 | jxl::ColorTransform::kXYB; |
1110 | 0 | } else { |
1111 | 0 | frame_settings->values.cparams.color_transform = |
1112 | 0 | static_cast<jxl::ColorTransform>(value); |
1113 | 0 | } |
1114 | 0 | return JXL_ENC_SUCCESS; |
1115 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE: |
1116 | 0 | if (value < -1 || value > 41) { |
1117 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1118 | 0 | "Option value has to be in [-1..41]"); |
1119 | 0 | } |
1120 | 0 | frame_settings->values.cparams.colorspace = value; |
1121 | 0 | return JXL_ENC_SUCCESS; |
1122 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE: |
1123 | 0 | if (value < -1 || value > 3) { |
1124 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1125 | 0 | "Option value has to be in [-1..3]"); |
1126 | 0 | } |
1127 | | // TODO(lode): the default behavior of this parameter for cjxl is |
1128 | | // to choose 1 or 2 depending on the situation. This behavior needs to be |
1129 | | // implemented either in the C++ library by allowing to set this to -1, or |
1130 | | // kept in cjxl and set it to 1 or 2 using this API. |
1131 | 0 | if (value == -1) { |
1132 | 0 | frame_settings->values.cparams.modular_group_size_shift = 1; |
1133 | 0 | } else { |
1134 | 0 | frame_settings->values.cparams.modular_group_size_shift = value; |
1135 | 0 | } |
1136 | 0 | return JXL_ENC_SUCCESS; |
1137 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR: |
1138 | 0 | if (value < -1 || value > 15) { |
1139 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1140 | 0 | "Option value has to be in [-1..15]"); |
1141 | 0 | } |
1142 | 0 | frame_settings->values.cparams.options.predictor = |
1143 | 0 | static_cast<jxl::Predictor>(value); |
1144 | 0 | return JXL_ENC_SUCCESS; |
1145 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS: |
1146 | | // The max allowed value can in theory be higher. However, it depends on |
1147 | | // the effort setting. 11 is the highest safe value that doesn't cause |
1148 | | // tree_samples to be >= 64 in the encoder. The specification may allow |
1149 | | // more than this. With more fine tuning higher values could be allowed. |
1150 | | // For N-channel images, the largest useful value is N-1. |
1151 | 0 | if (value < -1 || value > 11) { |
1152 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1153 | 0 | "Option value has to be in [-1..11]"); |
1154 | 0 | } |
1155 | 0 | if (value == -1) { |
1156 | 0 | frame_settings->values.cparams.options.max_properties = 0; |
1157 | 0 | } else { |
1158 | 0 | frame_settings->values.cparams.options.max_properties = value; |
1159 | 0 | } |
1160 | 0 | return JXL_ENC_SUCCESS; |
1161 | 0 | case JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL: |
1162 | 0 | if (value == -1) { |
1163 | 0 | frame_settings->values.cparams.force_cfl_jpeg_recompression = true; |
1164 | 0 | } else { |
1165 | 0 | frame_settings->values.cparams.force_cfl_jpeg_recompression = value; |
1166 | 0 | } |
1167 | 0 | return JXL_ENC_SUCCESS; |
1168 | 0 | case JXL_ENC_FRAME_INDEX_BOX: |
1169 | 0 | frame_settings->values.frame_index_box = true; |
1170 | 0 | return JXL_ENC_SUCCESS; |
1171 | 0 | case JXL_ENC_FRAME_SETTING_PHOTON_NOISE: |
1172 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, |
1173 | 0 | "Float option, try setting it with " |
1174 | 0 | "JxlEncoderFrameSettingsSetFloatOption"); |
1175 | 0 | default: |
1176 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, |
1177 | 0 | "Unknown option"); |
1178 | 0 | } |
1179 | 0 | } |
1180 | | |
1181 | | JxlEncoderStatus JxlEncoderFrameSettingsSetFloatOption( |
1182 | | JxlEncoderFrameSettings* frame_settings, JxlEncoderFrameSettingId option, |
1183 | 0 | float value) { |
1184 | 0 | switch (option) { |
1185 | 0 | case JXL_ENC_FRAME_SETTING_PHOTON_NOISE: |
1186 | 0 | if (value < 0) return JXL_ENC_ERROR; |
1187 | | // TODO(lode): add encoder setting to set the 8 floating point values of |
1188 | | // the noise synthesis parameters per frame for more fine grained control. |
1189 | 0 | frame_settings->values.cparams.photon_noise_iso = value; |
1190 | 0 | return JXL_ENC_SUCCESS; |
1191 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_MA_TREE_LEARNING_PERCENT: |
1192 | 0 | if (value < -1.f || value > 100.f) { |
1193 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1194 | 0 | "Option value has to be smaller than 100"); |
1195 | 0 | } |
1196 | | // This value is called "iterations" or "nb_repeats" in cjxl, but is in |
1197 | | // fact a fraction in range 0.0-1.0, with the default value 0.5. |
1198 | | // Convert from floating point percentage to floating point fraction here. |
1199 | 0 | if (value < -.5f) { |
1200 | | // TODO(lode): for this and many other settings (also in |
1201 | | // JxlEncoderFrameSettingsSetOption), avoid duplicating the default |
1202 | | // values here and in enc_params.h and options.h, have one location |
1203 | | // where the defaults are specified. |
1204 | 0 | frame_settings->values.cparams.options.nb_repeats = 0.5f; |
1205 | 0 | } else { |
1206 | 0 | frame_settings->values.cparams.options.nb_repeats = value * 0.01f; |
1207 | 0 | } |
1208 | 0 | return JXL_ENC_SUCCESS; |
1209 | 0 | case JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GLOBAL_PERCENT: |
1210 | 0 | if (value < -1.f || value > 100.f) { |
1211 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1212 | 0 | "Option value has to be in [-1..100]"); |
1213 | 0 | } |
1214 | 0 | if (value < -.5f) { |
1215 | 0 | frame_settings->values.cparams.channel_colors_pre_transform_percent = |
1216 | 0 | 95.0f; |
1217 | 0 | } else { |
1218 | 0 | frame_settings->values.cparams.channel_colors_pre_transform_percent = |
1219 | 0 | value; |
1220 | 0 | } |
1221 | 0 | return JXL_ENC_SUCCESS; |
1222 | 0 | case JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT: |
1223 | 0 | if (value < -1.f || value > 100.f) { |
1224 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1225 | 0 | "Option value has to be in [-1..100]"); |
1226 | 0 | } |
1227 | 0 | if (value < -.5f) { |
1228 | 0 | frame_settings->values.cparams.channel_colors_percent = 80.0f; |
1229 | 0 | } else { |
1230 | 0 | frame_settings->values.cparams.channel_colors_percent = value; |
1231 | 0 | } |
1232 | 0 | return JXL_ENC_SUCCESS; |
1233 | 0 | case JXL_ENC_FRAME_SETTING_EFFORT: |
1234 | 0 | case JXL_ENC_FRAME_SETTING_DECODING_SPEED: |
1235 | 0 | case JXL_ENC_FRAME_SETTING_RESAMPLING: |
1236 | 0 | case JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING: |
1237 | 0 | case JXL_ENC_FRAME_SETTING_ALREADY_DOWNSAMPLED: |
1238 | 0 | case JXL_ENC_FRAME_SETTING_NOISE: |
1239 | 0 | case JXL_ENC_FRAME_SETTING_DOTS: |
1240 | 0 | case JXL_ENC_FRAME_SETTING_PATCHES: |
1241 | 0 | case JXL_ENC_FRAME_SETTING_EPF: |
1242 | 0 | case JXL_ENC_FRAME_SETTING_GABORISH: |
1243 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR: |
1244 | 0 | case JXL_ENC_FRAME_SETTING_KEEP_INVISIBLE: |
1245 | 0 | case JXL_ENC_FRAME_SETTING_GROUP_ORDER: |
1246 | 0 | case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_X: |
1247 | 0 | case JXL_ENC_FRAME_SETTING_GROUP_ORDER_CENTER_Y: |
1248 | 0 | case JXL_ENC_FRAME_SETTING_RESPONSIVE: |
1249 | 0 | case JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC: |
1250 | 0 | case JXL_ENC_FRAME_SETTING_QPROGRESSIVE_AC: |
1251 | 0 | case JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC: |
1252 | 0 | case JXL_ENC_FRAME_SETTING_PALETTE_COLORS: |
1253 | 0 | case JXL_ENC_FRAME_SETTING_LOSSY_PALETTE: |
1254 | 0 | case JXL_ENC_FRAME_SETTING_COLOR_TRANSFORM: |
1255 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_COLOR_SPACE: |
1256 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE: |
1257 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR: |
1258 | 0 | case JXL_ENC_FRAME_SETTING_MODULAR_NB_PREV_CHANNELS: |
1259 | 0 | case JXL_ENC_FRAME_SETTING_JPEG_RECON_CFL: |
1260 | 0 | case JXL_ENC_FRAME_INDEX_BOX: |
1261 | 0 | case JXL_ENC_FRAME_SETTING_BROTLI_EFFORT: |
1262 | 0 | case JXL_ENC_FRAME_SETTING_FILL_ENUM: |
1263 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, |
1264 | 0 | "Int option, try setting it with " |
1265 | 0 | "JxlEncoderFrameSettingsSetOption"); |
1266 | 0 | default: |
1267 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_NOT_SUPPORTED, |
1268 | 0 | "Unknown option"); |
1269 | 0 | } |
1270 | 0 | } |
1271 | 46 | JxlEncoder* JxlEncoderCreate(const JxlMemoryManager* memory_manager) { |
1272 | 46 | JxlMemoryManager local_memory_manager; |
1273 | 46 | if (!jxl::MemoryManagerInit(&local_memory_manager, memory_manager)) { |
1274 | 0 | return nullptr; |
1275 | 0 | } |
1276 | | |
1277 | 46 | void* alloc = |
1278 | 46 | jxl::MemoryManagerAlloc(&local_memory_manager, sizeof(JxlEncoder)); |
1279 | 46 | if (!alloc) return nullptr; |
1280 | 46 | JxlEncoder* enc = new (alloc) JxlEncoder(); |
1281 | 46 | enc->memory_manager = local_memory_manager; |
1282 | | // TODO(sboukortt): add an API function to set this. |
1283 | 46 | enc->cms = jxl::GetJxlCms(); |
1284 | | |
1285 | | // Initialize all the field values. |
1286 | 46 | JxlEncoderReset(enc); |
1287 | | |
1288 | 46 | return enc; |
1289 | 46 | } |
1290 | | |
1291 | 138 | void JxlEncoderReset(JxlEncoder* enc) { |
1292 | 138 | enc->thread_pool.reset(); |
1293 | 138 | enc->input_queue.clear(); |
1294 | 138 | enc->num_queued_frames = 0; |
1295 | 138 | enc->num_queued_boxes = 0; |
1296 | 138 | enc->encoder_options.clear(); |
1297 | 138 | enc->output_byte_queue.clear(); |
1298 | 138 | enc->codestream_bytes_written_beginning_of_frame = 0; |
1299 | 138 | enc->codestream_bytes_written_end_of_frame = 0; |
1300 | 138 | enc->wrote_bytes = false; |
1301 | 138 | enc->jxlp_counter = 0; |
1302 | 138 | enc->metadata = jxl::CodecMetadata(); |
1303 | 138 | enc->last_used_cparams = jxl::CompressParams(); |
1304 | 138 | enc->frames_closed = false; |
1305 | 138 | enc->boxes_closed = false; |
1306 | 138 | enc->basic_info_set = false; |
1307 | 138 | enc->color_encoding_set = false; |
1308 | 138 | enc->intensity_target_set = false; |
1309 | 138 | enc->use_container = false; |
1310 | 138 | enc->use_boxes = false; |
1311 | 138 | enc->codestream_level = -1; |
1312 | 138 | JxlEncoderInitBasicInfo(&enc->basic_info); |
1313 | 138 | } |
1314 | | |
1315 | 46 | void JxlEncoderDestroy(JxlEncoder* enc) { |
1316 | 46 | if (enc) { |
1317 | 46 | JxlMemoryManager local_memory_manager = enc->memory_manager; |
1318 | | // Call destructor directly since custom free function is used. |
1319 | 46 | enc->~JxlEncoder(); |
1320 | 46 | jxl::MemoryManagerFree(&local_memory_manager, enc); |
1321 | 46 | } |
1322 | 46 | } |
1323 | | |
1324 | 0 | JxlEncoderError JxlEncoderGetError(JxlEncoder* enc) { return enc->error; } |
1325 | | |
1326 | | JxlEncoderStatus JxlEncoderUseContainer(JxlEncoder* enc, |
1327 | 0 | JXL_BOOL use_container) { |
1328 | 0 | if (enc->wrote_bytes) { |
1329 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
1330 | 0 | "this setting can only be set at the beginning"); |
1331 | 0 | } |
1332 | 0 | enc->use_container = static_cast<bool>(use_container); |
1333 | 0 | return JXL_ENC_SUCCESS; |
1334 | 0 | } |
1335 | | |
1336 | | JxlEncoderStatus JxlEncoderStoreJPEGMetadata(JxlEncoder* enc, |
1337 | 0 | JXL_BOOL store_jpeg_metadata) { |
1338 | 0 | if (enc->wrote_bytes) { |
1339 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
1340 | 0 | "this setting can only be set at the beginning"); |
1341 | 0 | } |
1342 | 0 | enc->store_jpeg_metadata = static_cast<bool>(store_jpeg_metadata); |
1343 | 0 | return JXL_ENC_SUCCESS; |
1344 | 0 | } |
1345 | | |
1346 | 92 | JxlEncoderStatus JxlEncoderSetCodestreamLevel(JxlEncoder* enc, int level) { |
1347 | 92 | if (level != -1 && level != 5 && level != 10) { |
1348 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_NOT_SUPPORTED, "invalid level"); |
1349 | 0 | } |
1350 | 92 | if (enc->wrote_bytes) { |
1351 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
1352 | 0 | "this setting can only be set at the beginning"); |
1353 | 0 | } |
1354 | 92 | enc->codestream_level = level; |
1355 | 92 | return JXL_ENC_SUCCESS; |
1356 | 92 | } |
1357 | | |
1358 | 0 | int JxlEncoderGetRequiredCodestreamLevel(const JxlEncoder* enc) { |
1359 | 0 | return VerifyLevelSettings(enc, nullptr); |
1360 | 0 | } |
1361 | | |
1362 | 0 | void JxlEncoderSetCms(JxlEncoder* enc, JxlCmsInterface cms) { |
1363 | 0 | jxl::msan::MemoryIsInitialized(&cms, sizeof(cms)); |
1364 | 0 | enc->cms = cms; |
1365 | 0 | } |
1366 | | |
1367 | | JxlEncoderStatus JxlEncoderSetParallelRunner(JxlEncoder* enc, |
1368 | | JxlParallelRunner parallel_runner, |
1369 | 92 | void* parallel_runner_opaque) { |
1370 | 92 | if (enc->thread_pool) { |
1371 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
1372 | 0 | "parallel runner already set"); |
1373 | 0 | } |
1374 | 92 | enc->thread_pool = jxl::MemoryManagerMakeUnique<jxl::ThreadPool>( |
1375 | 92 | &enc->memory_manager, parallel_runner, parallel_runner_opaque); |
1376 | 92 | if (!enc->thread_pool) { |
1377 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_GENERIC, |
1378 | 0 | "error setting parallel runner"); |
1379 | 0 | } |
1380 | 92 | return JXL_ENC_SUCCESS; |
1381 | 92 | } |
1382 | | |
1383 | | namespace { |
1384 | | JxlEncoderStatus GetCurrentDimensions( |
1385 | | const JxlEncoderFrameSettings* frame_settings, size_t& xsize, |
1386 | 92 | size_t& ysize) { |
1387 | 92 | xsize = frame_settings->enc->metadata.xsize(); |
1388 | 92 | ysize = frame_settings->enc->metadata.ysize(); |
1389 | 92 | if (frame_settings->values.header.layer_info.have_crop) { |
1390 | 0 | xsize = frame_settings->values.header.layer_info.xsize; |
1391 | 0 | ysize = frame_settings->values.header.layer_info.ysize; |
1392 | 0 | } |
1393 | 92 | if (frame_settings->values.cparams.already_downsampled) { |
1394 | 0 | size_t factor = frame_settings->values.cparams.resampling; |
1395 | 0 | xsize = jxl::DivCeil(xsize, factor); |
1396 | 0 | ysize = jxl::DivCeil(ysize, factor); |
1397 | 0 | } |
1398 | 92 | if (xsize == 0 || ysize == 0) { |
1399 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1400 | 0 | "zero-sized frame is not allowed"); |
1401 | 0 | } |
1402 | 92 | return JXL_ENC_SUCCESS; |
1403 | 92 | } |
1404 | | } // namespace |
1405 | | |
1406 | | JxlEncoderStatus JxlEncoderAddJPEGFrame( |
1407 | | const JxlEncoderFrameSettings* frame_settings, const uint8_t* buffer, |
1408 | 0 | size_t size) { |
1409 | 0 | if (frame_settings->enc->frames_closed) { |
1410 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1411 | 0 | "Frame input is already closed"); |
1412 | 0 | } |
1413 | | |
1414 | 0 | jxl::CodecInOut io; |
1415 | 0 | if (!jxl::jpeg::DecodeImageJPG(jxl::Span<const uint8_t>(buffer, size), &io)) { |
1416 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_BAD_INPUT, |
1417 | 0 | "Error during decode of input JPEG"); |
1418 | 0 | } |
1419 | | |
1420 | 0 | if (!frame_settings->enc->color_encoding_set) { |
1421 | 0 | if (!SetColorEncodingFromJpegData( |
1422 | 0 | *io.Main().jpeg_data, |
1423 | 0 | &frame_settings->enc->metadata.m.color_encoding)) { |
1424 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_BAD_INPUT, |
1425 | 0 | "Error in input JPEG color space"); |
1426 | 0 | } |
1427 | 0 | } |
1428 | | |
1429 | 0 | if (!frame_settings->enc->basic_info_set) { |
1430 | 0 | JxlBasicInfo basic_info; |
1431 | 0 | JxlEncoderInitBasicInfo(&basic_info); |
1432 | 0 | basic_info.xsize = io.Main().jpeg_data->width; |
1433 | 0 | basic_info.ysize = io.Main().jpeg_data->height; |
1434 | 0 | basic_info.uses_original_profile = true; |
1435 | 0 | if (JxlEncoderSetBasicInfo(frame_settings->enc, &basic_info) != |
1436 | 0 | JXL_ENC_SUCCESS) { |
1437 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1438 | 0 | "Error setting basic info"); |
1439 | 0 | } |
1440 | 0 | } |
1441 | | |
1442 | 0 | if (frame_settings->enc->metadata.m.xyb_encoded) { |
1443 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1444 | 0 | "Can't XYB encode a lossless JPEG"); |
1445 | 0 | } |
1446 | 0 | if (!io.blobs.exif.empty()) { |
1447 | 0 | JxlOrientation orientation = static_cast<JxlOrientation>( |
1448 | 0 | frame_settings->enc->metadata.m.orientation); |
1449 | 0 | jxl::InterpretExif(io.blobs.exif, &orientation); |
1450 | 0 | frame_settings->enc->metadata.m.orientation = orientation; |
1451 | |
|
1452 | 0 | size_t exif_size = io.blobs.exif.size(); |
1453 | | // Exif data in JPEG is limited to 64k |
1454 | 0 | if (exif_size > 0xFFFF) { |
1455 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1456 | 0 | "Exif larger than possible in JPEG?"); |
1457 | 0 | } |
1458 | 0 | exif_size += 4; // prefix 4 zero bytes for tiff offset |
1459 | 0 | std::vector<uint8_t> exif(exif_size); |
1460 | 0 | memcpy(exif.data() + 4, io.blobs.exif.data(), io.blobs.exif.size()); |
1461 | 0 | JxlEncoderUseBoxes(frame_settings->enc); |
1462 | 0 | JxlEncoderAddBox(frame_settings->enc, "Exif", exif.data(), exif_size, |
1463 | | /*compress_box=*/JXL_TRUE); |
1464 | 0 | } |
1465 | 0 | if (!io.blobs.xmp.empty()) { |
1466 | 0 | JxlEncoderUseBoxes(frame_settings->enc); |
1467 | 0 | JxlEncoderAddBox(frame_settings->enc, "xml ", io.blobs.xmp.data(), |
1468 | 0 | io.blobs.xmp.size(), /*compress_box=*/JXL_TRUE); |
1469 | 0 | } |
1470 | 0 | if (!io.blobs.jumbf.empty()) { |
1471 | 0 | JxlEncoderUseBoxes(frame_settings->enc); |
1472 | 0 | JxlEncoderAddBox(frame_settings->enc, "jumb", io.blobs.jumbf.data(), |
1473 | 0 | io.blobs.jumbf.size(), /*compress_box=*/JXL_TRUE); |
1474 | 0 | } |
1475 | 0 | if (frame_settings->enc->store_jpeg_metadata) { |
1476 | 0 | jxl::jpeg::JPEGData data_in = *io.Main().jpeg_data; |
1477 | 0 | jxl::PaddedBytes jpeg_data; |
1478 | 0 | if (!jxl::jpeg::EncodeJPEGData(data_in, &jpeg_data, |
1479 | 0 | frame_settings->values.cparams)) { |
1480 | 0 | return JXL_API_ERROR( |
1481 | 0 | frame_settings->enc, JXL_ENC_ERR_JBRD, |
1482 | 0 | "JPEG bitstream reconstruction data cannot be encoded"); |
1483 | 0 | } |
1484 | 0 | frame_settings->enc->jpeg_metadata = std::vector<uint8_t>( |
1485 | 0 | jpeg_data.data(), jpeg_data.data() + jpeg_data.size()); |
1486 | 0 | } |
1487 | | |
1488 | 0 | auto queued_frame = jxl::MemoryManagerMakeUnique<jxl::JxlEncoderQueuedFrame>( |
1489 | 0 | &frame_settings->enc->memory_manager, |
1490 | | // JxlEncoderQueuedFrame is a struct with no constructors, so we use the |
1491 | | // default move constructor there. |
1492 | 0 | jxl::JxlEncoderQueuedFrame{ |
1493 | 0 | frame_settings->values, |
1494 | 0 | jxl::ImageBundle(&frame_settings->enc->metadata.m), |
1495 | 0 | {}}); |
1496 | 0 | if (!queued_frame) { |
1497 | | // TODO(jon): when can this happen? is this an API usage error? |
1498 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1499 | 0 | "No frame queued?"); |
1500 | 0 | } |
1501 | 0 | queued_frame->frame.SetFromImage(std::move(*io.Main().color()), |
1502 | 0 | io.Main().c_current()); |
1503 | 0 | size_t xsize, ysize; |
1504 | 0 | if (GetCurrentDimensions(frame_settings, xsize, ysize) != JXL_ENC_SUCCESS) { |
1505 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1506 | 0 | "bad dimensions"); |
1507 | 0 | } |
1508 | 0 | if (xsize != static_cast<size_t>(io.Main().jpeg_data->width) || |
1509 | 0 | ysize != static_cast<size_t>(io.Main().jpeg_data->height)) { |
1510 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1511 | 0 | "JPEG dimensions don't match frame dimensions"); |
1512 | 0 | } |
1513 | 0 | std::vector<jxl::ImageF> extra_channels( |
1514 | 0 | frame_settings->enc->metadata.m.num_extra_channels); |
1515 | 0 | for (auto& extra_channel : extra_channels) { |
1516 | 0 | extra_channel = jxl::ImageF(xsize, ysize); |
1517 | 0 | queued_frame->ec_initialized.push_back(0); |
1518 | 0 | } |
1519 | 0 | queued_frame->frame.SetExtraChannels(std::move(extra_channels)); |
1520 | 0 | queued_frame->frame.jpeg_data = std::move(io.Main().jpeg_data); |
1521 | 0 | queued_frame->frame.color_transform = io.Main().color_transform; |
1522 | 0 | queued_frame->frame.chroma_subsampling = io.Main().chroma_subsampling; |
1523 | |
|
1524 | 0 | QueueFrame(frame_settings, queued_frame); |
1525 | 0 | return JXL_ENC_SUCCESS; |
1526 | 0 | } |
1527 | | |
1528 | | JxlEncoderStatus JxlEncoderAddImageFrame( |
1529 | | const JxlEncoderFrameSettings* frame_settings, |
1530 | 92 | const JxlPixelFormat* pixel_format, const void* buffer, size_t size) { |
1531 | 92 | if (!frame_settings->enc->basic_info_set || |
1532 | 92 | (!frame_settings->enc->color_encoding_set && |
1533 | 92 | !frame_settings->enc->metadata.m.xyb_encoded)) { |
1534 | | // Basic Info must be set, and color encoding must be set directly, |
1535 | | // or set to XYB via JxlBasicInfo.uses_original_profile = JXL_FALSE |
1536 | | // Otherwise, this is an API misuse. |
1537 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1538 | 0 | "Basic info or color encoding not set yet"); |
1539 | 0 | } |
1540 | | |
1541 | 92 | if (frame_settings->enc->frames_closed) { |
1542 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1543 | 0 | "Frame input already closed"); |
1544 | 0 | } |
1545 | 92 | if (pixel_format->num_channels < 3) { |
1546 | 0 | if (frame_settings->enc->basic_info.num_color_channels != 1) { |
1547 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1548 | 0 | "Grayscale pixel format input for an RGB image"); |
1549 | 0 | } |
1550 | 92 | } else { |
1551 | 92 | if (frame_settings->enc->basic_info.num_color_channels != 3) { |
1552 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1553 | 0 | "RGB pixel format input for a grayscale image"); |
1554 | 0 | } |
1555 | 92 | } |
1556 | | |
1557 | 92 | auto queued_frame = jxl::MemoryManagerMakeUnique<jxl::JxlEncoderQueuedFrame>( |
1558 | 92 | &frame_settings->enc->memory_manager, |
1559 | | // JxlEncoderQueuedFrame is a struct with no constructors, so we use the |
1560 | | // default move constructor there. |
1561 | 92 | jxl::JxlEncoderQueuedFrame{ |
1562 | 92 | frame_settings->values, |
1563 | 92 | jxl::ImageBundle(&frame_settings->enc->metadata.m), |
1564 | 92 | {}}); |
1565 | | |
1566 | 92 | if (!queued_frame) { |
1567 | | // TODO(jon): when can this happen? is this an API usage error? |
1568 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1569 | 0 | "No frame queued?"); |
1570 | 0 | } |
1571 | | |
1572 | 92 | jxl::ColorEncoding c_current; |
1573 | 92 | if (!frame_settings->enc->color_encoding_set) { |
1574 | 0 | if ((pixel_format->data_type == JXL_TYPE_FLOAT) || |
1575 | 0 | (pixel_format->data_type == JXL_TYPE_FLOAT16)) { |
1576 | 0 | c_current = |
1577 | 0 | jxl::ColorEncoding::LinearSRGB(pixel_format->num_channels < 3); |
1578 | 0 | } else { |
1579 | 0 | c_current = jxl::ColorEncoding::SRGB(pixel_format->num_channels < 3); |
1580 | 0 | } |
1581 | 92 | } else { |
1582 | 92 | c_current = frame_settings->enc->metadata.m.color_encoding; |
1583 | 92 | } |
1584 | 92 | uint32_t num_channels = pixel_format->num_channels; |
1585 | 92 | size_t has_interleaved_alpha = |
1586 | 92 | static_cast<size_t>(num_channels == 2 || num_channels == 4); |
1587 | 92 | if (has_interleaved_alpha > |
1588 | 92 | frame_settings->enc->metadata.m.num_extra_channels) { |
1589 | 0 | return JXL_API_ERROR( |
1590 | 0 | frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1591 | 0 | "number of extra channels mismatch (need 1 extra channel for alpha)"); |
1592 | 0 | } |
1593 | 92 | size_t xsize, ysize; |
1594 | 92 | if (GetCurrentDimensions(frame_settings, xsize, ysize) != JXL_ENC_SUCCESS) { |
1595 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1596 | 0 | "bad dimensions"); |
1597 | 0 | } |
1598 | 92 | std::vector<jxl::ImageF> extra_channels( |
1599 | 92 | frame_settings->enc->metadata.m.num_extra_channels); |
1600 | 92 | for (auto& extra_channel : extra_channels) { |
1601 | 0 | extra_channel = jxl::ImageF(xsize, ysize); |
1602 | 0 | } |
1603 | 92 | queued_frame->frame.SetExtraChannels(std::move(extra_channels)); |
1604 | 92 | for (auto& ec_info : frame_settings->enc->metadata.m.extra_channel_info) { |
1605 | 0 | if (has_interleaved_alpha && ec_info.type == jxl::ExtraChannel::kAlpha) { |
1606 | 0 | queued_frame->ec_initialized.push_back(1); |
1607 | 0 | has_interleaved_alpha = 0; // only first Alpha is initialized |
1608 | 0 | } else { |
1609 | 0 | queued_frame->ec_initialized.push_back(0); |
1610 | 0 | } |
1611 | 0 | } |
1612 | 92 | queued_frame->frame.origin.x0 = |
1613 | 92 | frame_settings->values.header.layer_info.crop_x0; |
1614 | 92 | queued_frame->frame.origin.y0 = |
1615 | 92 | frame_settings->values.header.layer_info.crop_y0; |
1616 | 92 | queued_frame->frame.use_for_next_frame = |
1617 | 92 | (frame_settings->values.header.layer_info.save_as_reference != 0u); |
1618 | 92 | queued_frame->frame.blendmode = |
1619 | 92 | frame_settings->values.header.layer_info.blend_info.blendmode == |
1620 | 92 | JXL_BLEND_REPLACE |
1621 | 92 | ? jxl::BlendMode::kReplace |
1622 | 92 | : jxl::BlendMode::kBlend; |
1623 | 92 | queued_frame->frame.blend = |
1624 | 92 | frame_settings->values.header.layer_info.blend_info.source > 0; |
1625 | | |
1626 | 92 | if (!jxl::BufferToImageBundle(*pixel_format, xsize, ysize, buffer, size, |
1627 | 92 | frame_settings->enc->thread_pool.get(), |
1628 | 92 | c_current, &(queued_frame->frame))) { |
1629 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1630 | 0 | "Invalid input buffer"); |
1631 | 0 | } |
1632 | 92 | if (frame_settings->values.lossless && |
1633 | 92 | frame_settings->enc->metadata.m.xyb_encoded) { |
1634 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1635 | 0 | "Set use_original_profile=true for lossless encoding"); |
1636 | 0 | } |
1637 | 92 | queued_frame->option_values.cparams.level = |
1638 | 92 | frame_settings->enc->codestream_level; |
1639 | | |
1640 | 92 | QueueFrame(frame_settings, queued_frame); |
1641 | 92 | return JXL_ENC_SUCCESS; |
1642 | 92 | } |
1643 | | |
1644 | 0 | JxlEncoderStatus JxlEncoderUseBoxes(JxlEncoder* enc) { |
1645 | 0 | if (enc->wrote_bytes) { |
1646 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
1647 | 0 | "this setting can only be set at the beginning"); |
1648 | 0 | } |
1649 | 0 | enc->use_boxes = true; |
1650 | 0 | return JXL_ENC_SUCCESS; |
1651 | 0 | } |
1652 | | |
1653 | | JxlEncoderStatus JxlEncoderAddBox(JxlEncoder* enc, const JxlBoxType type, |
1654 | | const uint8_t* contents, size_t size, |
1655 | 0 | JXL_BOOL compress_box) { |
1656 | 0 | if (!enc->use_boxes) { |
1657 | 0 | return JXL_API_ERROR( |
1658 | 0 | enc, JXL_ENC_ERR_API_USAGE, |
1659 | 0 | "must set JxlEncoderUseBoxes at the beginning to add boxes"); |
1660 | 0 | } |
1661 | 0 | if (compress_box) { |
1662 | 0 | if (memcmp("jxl", type, 3) == 0) { |
1663 | 0 | return JXL_API_ERROR( |
1664 | 0 | enc, JXL_ENC_ERR_API_USAGE, |
1665 | 0 | "brob box may not contain a type starting with \"jxl\""); |
1666 | 0 | } |
1667 | 0 | if (memcmp("jbrd", type, 4) == 0) { |
1668 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
1669 | 0 | "jbrd box may not be brob compressed"); |
1670 | 0 | } |
1671 | 0 | if (memcmp("brob", type, 4) == 0) { |
1672 | | // The compress_box will compress an existing non-brob box into a brob |
1673 | | // box. If already giving a valid brotli-compressed brob box, set |
1674 | | // compress_box to false since it is already compressed. |
1675 | 0 | return JXL_API_ERROR(enc, JXL_ENC_ERR_API_USAGE, |
1676 | 0 | "a brob box cannot contain another brob box"); |
1677 | 0 | } |
1678 | 0 | } |
1679 | | |
1680 | 0 | auto box = jxl::MemoryManagerMakeUnique<jxl::JxlEncoderQueuedBox>( |
1681 | 0 | &enc->memory_manager); |
1682 | |
|
1683 | 0 | box->type = jxl::MakeBoxType(type); |
1684 | 0 | box->contents.assign(contents, contents + size); |
1685 | 0 | box->compress_box = !!compress_box; |
1686 | 0 | QueueBox(enc, box); |
1687 | 0 | return JXL_ENC_SUCCESS; |
1688 | 0 | } |
1689 | | |
1690 | | JXL_EXPORT JxlEncoderStatus JxlEncoderSetExtraChannelBuffer( |
1691 | | const JxlEncoderOptions* frame_settings, const JxlPixelFormat* pixel_format, |
1692 | 0 | const void* buffer, size_t size, uint32_t index) { |
1693 | 0 | if (index >= frame_settings->enc->metadata.m.num_extra_channels) { |
1694 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1695 | 0 | "Invalid value for the index of extra channel"); |
1696 | 0 | } |
1697 | 0 | if (!frame_settings->enc->basic_info_set || |
1698 | 0 | !frame_settings->enc->color_encoding_set) { |
1699 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1700 | 0 | "Basic info has to be set first"); |
1701 | 0 | } |
1702 | 0 | if (frame_settings->enc->input_queue.empty()) { |
1703 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1704 | 0 | "First add image frame, then extra channels"); |
1705 | 0 | } |
1706 | 0 | if (frame_settings->enc->frames_closed) { |
1707 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1708 | 0 | "Frame input already closed"); |
1709 | 0 | } |
1710 | 0 | size_t xsize, ysize; |
1711 | 0 | if (GetCurrentDimensions(frame_settings, xsize, ysize) != JXL_ENC_SUCCESS) { |
1712 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_GENERIC, |
1713 | 0 | "bad dimensions"); |
1714 | 0 | } |
1715 | 0 | if (!jxl::BufferToImageF(*pixel_format, xsize, ysize, buffer, size, |
1716 | 0 | frame_settings->enc->thread_pool.get(), |
1717 | 0 | &frame_settings->enc->input_queue.back() |
1718 | 0 | .frame->frame.extra_channels()[index])) { |
1719 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1720 | 0 | "Failed to set buffer for extra channel"); |
1721 | 0 | } |
1722 | 0 | frame_settings->enc->input_queue.back().frame->ec_initialized[index] = 1; |
1723 | |
|
1724 | 0 | return JXL_ENC_SUCCESS; |
1725 | 0 | } |
1726 | | |
1727 | 0 | void JxlEncoderCloseFrames(JxlEncoder* enc) { enc->frames_closed = true; } |
1728 | | |
1729 | 0 | void JxlEncoderCloseBoxes(JxlEncoder* enc) { enc->boxes_closed = true; } |
1730 | | |
1731 | 0 | void JxlEncoderCloseInput(JxlEncoder* enc) { |
1732 | 0 | JxlEncoderCloseFrames(enc); |
1733 | 0 | JxlEncoderCloseBoxes(enc); |
1734 | 0 | } |
1735 | | JxlEncoderStatus JxlEncoderProcessOutput(JxlEncoder* enc, uint8_t** next_out, |
1736 | 5.61k | size_t* avail_out) { |
1737 | 11.3k | while (*avail_out > 0 && |
1738 | 11.3k | (!enc->output_byte_queue.empty() || !enc->input_queue.empty())) { |
1739 | 5.70k | if (!enc->output_byte_queue.empty()) { |
1740 | 5.61k | size_t to_copy = std::min(*avail_out, enc->output_byte_queue.size()); |
1741 | 5.61k | std::copy_n(enc->output_byte_queue.begin(), to_copy, *next_out); |
1742 | 5.61k | *next_out += to_copy; |
1743 | 5.61k | *avail_out -= to_copy; |
1744 | 5.61k | enc->output_byte_queue.erase(enc->output_byte_queue.begin(), |
1745 | 5.61k | enc->output_byte_queue.begin() + to_copy); |
1746 | 5.61k | } else if (!enc->input_queue.empty()) { |
1747 | 92 | if (enc->RefillOutputByteQueue() != JXL_ENC_SUCCESS) { |
1748 | 0 | return JXL_ENC_ERROR; |
1749 | 0 | } |
1750 | 92 | } |
1751 | 5.70k | } |
1752 | | |
1753 | 5.61k | if (!enc->output_byte_queue.empty() || !enc->input_queue.empty()) { |
1754 | 5.52k | return JXL_ENC_NEED_MORE_OUTPUT; |
1755 | 5.52k | } |
1756 | 92 | return JXL_ENC_SUCCESS; |
1757 | 5.61k | } |
1758 | | |
1759 | | JxlEncoderStatus JxlEncoderSetFrameHeader(JxlEncoderOptions* frame_settings, |
1760 | 92 | const JxlFrameHeader* frame_header) { |
1761 | 92 | if (frame_header->layer_info.blend_info.source > 3) { |
1762 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1763 | 0 | "invalid blending source index"); |
1764 | 0 | } |
1765 | | // If there are no extra channels, it's ok for the value to be 0. |
1766 | 92 | if (frame_header->layer_info.blend_info.alpha != 0 && |
1767 | 92 | frame_header->layer_info.blend_info.alpha >= |
1768 | 0 | frame_settings->enc->metadata.m.extra_channel_info.size()) { |
1769 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1770 | 0 | "alpha blend channel index out of bounds"); |
1771 | 0 | } |
1772 | | |
1773 | 92 | frame_settings->values.header = *frame_header; |
1774 | | // Setting the frame header resets the frame name, it must be set again with |
1775 | | // JxlEncoderSetFrameName if desired. |
1776 | 92 | frame_settings->values.frame_name = ""; |
1777 | | |
1778 | 92 | return JXL_ENC_SUCCESS; |
1779 | 92 | } |
1780 | | |
1781 | | JxlEncoderStatus JxlEncoderSetExtraChannelBlendInfo( |
1782 | | JxlEncoderOptions* frame_settings, size_t index, |
1783 | 0 | const JxlBlendInfo* blend_info) { |
1784 | 0 | if (index >= frame_settings->enc->metadata.m.num_extra_channels) { |
1785 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1786 | 0 | "Invalid value for the index of extra channel"); |
1787 | 0 | } |
1788 | | |
1789 | 0 | if (frame_settings->values.extra_channel_blend_info.size() != |
1790 | 0 | frame_settings->enc->metadata.m.num_extra_channels) { |
1791 | 0 | JxlBlendInfo default_blend_info; |
1792 | 0 | JxlEncoderInitBlendInfo(&default_blend_info); |
1793 | 0 | frame_settings->values.extra_channel_blend_info.resize( |
1794 | 0 | frame_settings->enc->metadata.m.num_extra_channels, default_blend_info); |
1795 | 0 | } |
1796 | 0 | frame_settings->values.extra_channel_blend_info[index] = *blend_info; |
1797 | 0 | return JXL_ENC_SUCCESS; |
1798 | 0 | } |
1799 | | |
1800 | | JxlEncoderStatus JxlEncoderSetFrameName(JxlEncoderFrameSettings* frame_settings, |
1801 | 0 | const char* frame_name) { |
1802 | 0 | std::string str = frame_name ? frame_name : ""; |
1803 | 0 | if (str.size() > 1071) { |
1804 | 0 | return JXL_API_ERROR(frame_settings->enc, JXL_ENC_ERR_API_USAGE, |
1805 | 0 | "frame name can be max 1071 bytes long"); |
1806 | 0 | } |
1807 | 0 | frame_settings->values.frame_name = str; |
1808 | 0 | frame_settings->values.header.name_length = str.size(); |
1809 | 0 | return JXL_ENC_SUCCESS; |
1810 | 0 | } |
1811 | | |
1812 | | void JxlColorEncodingSetToSRGB(JxlColorEncoding* color_encoding, |
1813 | 0 | JXL_BOOL is_gray) { |
1814 | 0 | ConvertInternalToExternalColorEncoding(jxl::ColorEncoding::SRGB(is_gray), |
1815 | 0 | color_encoding); |
1816 | 0 | } |
1817 | | |
1818 | | void JxlColorEncodingSetToLinearSRGB(JxlColorEncoding* color_encoding, |
1819 | 0 | JXL_BOOL is_gray) { |
1820 | 0 | ConvertInternalToExternalColorEncoding( |
1821 | 0 | jxl::ColorEncoding::LinearSRGB(is_gray), color_encoding); |
1822 | 0 | } |