1 /*
2 * JPEG XL encoding support via libjxl
3 * Copyright (c) 2021 Leo Izen <leo.izen@gmail.com>
4 *
5 * This file is part of FFmpeg.
6 *
7 * FFmpeg is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU Lesser General Public
9 * License as published by the Free Software Foundation; either
10 * version 2.1 of the License, or (at your option) any later version.
11 *
12 * FFmpeg is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with FFmpeg; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20 */
21
22 /**
23 * @file
24 * JPEG XL encoder using libjxl
25 */
26
27 #include <string.h>
28
39
43
44 #include <jxl/encode.h>
45 #include <jxl/thread_parallel_runner.h>
47
60
61 /**
62 * Map a quality setting for -qscale roughly from libjpeg
63 * quality numbers to libjxl's butteraugli distance for
64 * photographic content.
65 *
66 * Setting distance explicitly is preferred, but this will
67 * allow qscale to be used as a fallback.
68 *
69 * This function is continuous and injective on [0, 100] which
70 * makes it monotonic.
71 *
72 * @param quality 0.0 to 100.0 quality setting, libjpeg quality
73 * @return Butteraugli distance between 0.0 and 15.0
74 */
76 {
78 return 0.0;
80 return (100.0 -
quality) * 0.10;
82 return 0.1 + (100.0 -
quality) * 0.09;
85 else
86 return 15.0;
87 }
88
89 /**
90 * Initalize the encoder on a per-frame basis. All of these need to be set
91 * once each time the encoder is reset, which it must be each frame to make
92 * the image2 muxer work.
93 *
94 * @return 0 upon success, negative on failure.
95 */
97 {
99
100 /* reset the encoder every frame for image2 muxer */
101 JxlEncoderReset(
ctx->encoder);
102
103 ctx->options = JxlEncoderFrameSettingsCreate(
ctx->encoder,
NULL);
107 }
108
109 /* This needs to be set each time the encoder is reset */
110 if (JxlEncoderSetParallelRunner(
ctx->encoder, JxlThreadParallelRunner,
ctx->runner)
111 != JXL_ENC_SUCCESS) {
114 }
115
116 /* these shouldn't fail, libjxl bug notwithstanding */
117 if (JxlEncoderFrameSettingsSetOption(
ctx->options, JXL_ENC_FRAME_SETTING_EFFORT,
ctx->effort)
118 != JXL_ENC_SUCCESS) {
121 }
122
123 /* check for negative, our default */
124 if (
ctx->distance < 0.0) {
125 /* use ffmpeg.c -q option if passed */
128 else
129 /* default 1.0 matches cjxl */
131 }
132
133 /*
134 * 0.01 is the minimum distance accepted for lossy
135 * interpreting any positive value less than this as minimum
136 */
137 if (
ctx->distance > 0.0 &&
ctx->distance < 0.01)
138 ctx->distance = 0.01;
139 if (JxlEncoderSetFrameDistance(
ctx->options,
ctx->distance) != JXL_ENC_SUCCESS) {
142 }
143
144 /*
145 * In theory the library should automatically enable modular if necessary,
146 * but it appears it won't at the moment due to a bug. This will still
147 * work even if that is patched.
148 */
149 if (JxlEncoderFrameSettingsSetOption(
ctx->options, JXL_ENC_FRAME_SETTING_MODULAR,
150 ctx->modular ||
ctx->distance <= 0.0 ? 1 : -1) != JXL_ENC_SUCCESS) {
153 }
154
155 return 0;
156 }
157
158 /**
159 * Global encoder initialization. This only needs to be run once,
160 * not every frame.
161 */
163 {
165 JxlMemoryManager manager;
166
168 ctx->encoder = JxlEncoderCreate(&manager);
172 }
173
178 }
179
180 ctx->buffer_size = 4096;
182
186 }
187
188 return 0;
189 }
190
191 /**
192 * Populate a JxlColorEncoding with the given enum AVColorPrimaries.
193 * @return < 0 upon failure, >= 0 upon success
194 */
196 {
198
199 switch (prm) {
201 jxl_color->primaries = JXL_PRIMARIES_SRGB;
202 jxl_color->white_point = JXL_WHITE_POINT_D65;
203 return 0;
205 jxl_color->primaries = JXL_PRIMARIES_2100;
206 jxl_color->white_point = JXL_WHITE_POINT_D65;
207 return 0;
209 jxl_color->primaries = JXL_PRIMARIES_P3;
210 jxl_color->white_point = JXL_WHITE_POINT_DCI;
211 return 0;
213 jxl_color->primaries = JXL_PRIMARIES_P3;
214 jxl_color->white_point = JXL_WHITE_POINT_D65;
215 return 0;
217 av_log(avctx,
AV_LOG_WARNING,
"Unknown primaries, assuming BT.709/sRGB. Colors may be wrong.\n");
218 jxl_color->primaries = JXL_PRIMARIES_SRGB;
219 jxl_color->white_point = JXL_WHITE_POINT_D65;
220 return 0;
221 }
222
226
227 jxl_color->primaries = JXL_PRIMARIES_CUSTOM;
228 jxl_color->white_point = JXL_WHITE_POINT_CUSTOM;
229
230 jxl_color->primaries_red_xy[0] =
av_q2d(
desc->prim.r.x);
231 jxl_color->primaries_red_xy[1] =
av_q2d(
desc->prim.r.y);
232 jxl_color->primaries_green_xy[0] =
av_q2d(
desc->prim.g.x);
233 jxl_color->primaries_green_xy[1] =
av_q2d(
desc->prim.g.y);
234 jxl_color->primaries_blue_xy[0] =
av_q2d(
desc->prim.b.x);
235 jxl_color->primaries_blue_xy[1] =
av_q2d(
desc->prim.b.y);
236 jxl_color->white_point_xy[0] =
av_q2d(
desc->wp.x);
237 jxl_color->white_point_xy[1] =
av_q2d(
desc->wp.y);
238
239 return 0;
240 }
241
242 /**
243 * Encode an entire frame. Currently animation, is not supported by
244 * this encoder, so this will always reinitialize a new still image
245 * and encode a one-frame image (for image2 and image2pipe).
246 */
248 {
253 JxlColorEncoding jxl_color;
254 JxlPixelFormat jxl_fmt;
255 int bits_per_sample;
256 #if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 8, 0)
257 JxlBitDepth jxl_bit_depth;
258 #endif
259 JxlEncoderStatus jret;
262 size_t bytes_written = 0;
263 uint8_t *next_out =
ctx->buffer;
265
270 }
271
272 /* populate the basic info settings */
273 JxlEncoderInitBasicInfo(&
info);
277 info.num_extra_channels = (jxl_fmt.num_channels + 1) % 2;
278 info.num_color_channels = jxl_fmt.num_channels -
info.num_extra_channels;
282 info.alpha_bits = (
info.num_extra_channels > 0) *
info.bits_per_sample;
284 info.exponent_bits_per_sample =
info.bits_per_sample > 16 ? 8 : 5;
285 info.alpha_exponent_bits =
info.alpha_bits ?
info.exponent_bits_per_sample : 0;
286 jxl_fmt.data_type =
info.bits_per_sample > 16 ? JXL_TYPE_FLOAT : JXL_TYPE_FLOAT16;
287 } else {
288 info.exponent_bits_per_sample = 0;
289 info.alpha_exponent_bits = 0;
290 jxl_fmt.data_type =
info.bits_per_sample <= 8 ? JXL_TYPE_UINT8 : JXL_TYPE_UINT16;
291 }
292
293 #if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 8, 0)
294 jxl_bit_depth.bits_per_sample = bits_per_sample;
295 jxl_bit_depth.type = JXL_BIT_DEPTH_FROM_PIXEL_FORMAT;
297 info.exponent_bits_per_sample : 0;
298 #endif
299
300 /* JPEG XL format itself does not support limited range */
303 av_log(avctx,
AV_LOG_WARNING,
"This encoder does not support limited (tv) range, colors will be wrong!\n");
306
307 /* bitexact lossless requires there to be no XYB transform */
308 info.uses_original_profile =
ctx->distance == 0.0 || !
ctx->xyb;
309 info.orientation =
frame->linesize[0] >= 0 ? JXL_ORIENT_IDENTITY : JXL_ORIENT_FLIP_VERTICAL;
310
311 if (JxlEncoderSetBasicInfo(
ctx->encoder, &
info) != JXL_ENC_SUCCESS) {
314 }
315
316 /* rendering intent doesn't matter here
317 * but libjxl will whine if we don't set it */
318 jxl_color.rendering_intent = JXL_RENDERING_INTENT_RELATIVE;
319
323 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_709;
324 break;
326 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;
327 break;
329 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
330 break;
332 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_DCI;
333 break;
335 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_PQ;
336 break;
338 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_HLG;
339 break;
341 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
342 jxl_color.gamma = 1/2.2f;
343 break;
345 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA;
346 jxl_color.gamma = 1/2.8f;
347 break;
348 default:
350 av_log(avctx,
AV_LOG_WARNING,
"Unknown transfer function, assuming Linear Light. Colors may be wrong.\n");
351 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR;
352 } else {
353 av_log(avctx,
AV_LOG_WARNING,
"Unknown transfer function, assuming IEC61966-2-1/sRGB. Colors may be wrong.\n");
354 jxl_color.transfer_function = JXL_TRANSFER_FUNCTION_SRGB;
355 }
356 }
357
358 /* This should be implied to be honest
359 * but a libjxl bug makes it fail otherwise */
360 if (
info.num_color_channels == 1)
361 jxl_color.color_space = JXL_COLOR_SPACE_GRAY;
362 else
363 jxl_color.color_space = JXL_COLOR_SPACE_RGB;
364
370
372 if (sd && sd->
size && JxlEncoderSetICCProfile(
ctx->encoder, sd->
data, sd->
size) != JXL_ENC_SUCCESS)
374 if (JxlEncoderSetColorEncoding(
ctx->encoder, &jxl_color) != JXL_ENC_SUCCESS)
376
377 #if JPEGXL_NUMERIC_VERSION >= JPEGXL_COMPUTE_NUMERIC_VERSION(0, 8, 0)
378 if (JxlEncoderSetFrameBitDepth(
ctx->options, &jxl_bit_depth) != JXL_ENC_SUCCESS)
380 #endif
381
382 /* depending on basic info, level 10 might
383 * be required instead of level 5 */
384 if (JxlEncoderGetRequiredCodestreamLevel(
ctx->encoder) > 5) {
385 if (JxlEncoderSetCodestreamLevel(
ctx->encoder, 10) != JXL_ENC_SUCCESS)
387 }
388
389 jxl_fmt.endianness = JXL_NATIVE_ENDIAN;
390 if (
frame->linesize[0] >= 0) {
391 jxl_fmt.align =
frame->linesize[0];
393 } else {
394 jxl_fmt.align = -
frame->linesize[0];
396 }
397
398 if (JxlEncoderAddImageFrame(
ctx->options, &jxl_fmt,
data, jxl_fmt.align *
info.ysize) != JXL_ENC_SUCCESS) {
401 }
402
403 /*
404 * Run this after the last frame in the image has been passed.
405 * TODO support animation
406 */
407 JxlEncoderCloseInput(
ctx->encoder);
408
409 while (1) {
410 jret = JxlEncoderProcessOutput(
ctx->encoder, &next_out, &
available);
411 if (jret == JXL_ENC_ERROR) {
414 }
416 /* all data passed has been encoded */
417 if (jret == JXL_ENC_SUCCESS)
418 break;
419 if (jret == JXL_ENC_NEED_MORE_OUTPUT) {
420 /*
421 * at the moment, libjxl has no way to
422 * tell us how much space it actually needs
423 * so we need to malloc loop
424 */
426 size_t new_size =
ctx->buffer_size * 2;
431 ctx->buffer_size = new_size;
432 next_out =
ctx->buffer + bytes_written;
434 continue;
435 }
438 }
439
443
444 memcpy(
pkt->
data,
ctx->buffer, bytes_written);
445 *got_packet = 1;
446
447 return 0;
448 }
449
451 {
453
455 JxlThreadParallelRunnerDestroy(
ctx->runner);
457
458 /*
459 * destroying the encoder also frees
460 * ctx->options so we don't need to
461 */
463 JxlEncoderDestroy(
ctx->encoder);
465
467
468 return 0;
469 }
470
471 #define OFFSET(x) offsetof(LibJxlEncodeContext, x)
472 #define VE AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM
473
476 { "distance", "Maximum Butteraugli distance (quality setting, "
479 {
"xyb",
"Use XYB-encoding for lossy images",
OFFSET(xyb),
482 };
483
489 };
490
514 },
516 .p.wrapper_name = "libjxl",
517 };