pixbufloader-jxl.c (28488B)
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/codestream_header.h> 7 #include <jxl/decode.h> 8 #include <jxl/encode.h> 9 #include <jxl/resizable_parallel_runner.h> 10 #include <jxl/types.h> 11 12 #define GDK_PIXBUF_ENABLE_BACKEND 13 #include <gdk-pixbuf/gdk-pixbuf.h> 14 #undef GDK_PIXBUF_ENABLE_BACKEND 15 16 G_BEGIN_DECLS 17 18 // Information about a single frame. 19 typedef struct { 20 uint64_t duration_ms; 21 GdkPixbuf *data; 22 gboolean decoded; 23 } GdkPixbufJxlAnimationFrame; 24 25 // Represent a whole JPEG XL animation; all its fields are owned; as a GObject, 26 // the Animation struct itself is reference counted (as are the GdkPixbufs for 27 // individual frames). 28 struct _GdkPixbufJxlAnimation { 29 GdkPixbufAnimation parent_instance; 30 31 // GDK interface implementation callbacks. 32 GdkPixbufModuleSizeFunc image_size_callback; 33 GdkPixbufModulePreparedFunc pixbuf_prepared_callback; 34 GdkPixbufModuleUpdatedFunc area_updated_callback; 35 gpointer user_data; 36 37 // All frames known so far; a frame is added when the JXL_DEC_FRAME event is 38 // received from the decoder; initially frame.decoded is FALSE, until 39 // the JXL_DEC_IMAGE event is received. 40 GArray *frames; 41 42 // JPEG XL decoder and related structures. 43 JxlParallelRunner *parallel_runner; 44 JxlDecoder *decoder; 45 JxlPixelFormat pixel_format; 46 47 // Decoding is `done` when JXL_DEC_SUCCESS is received; calling 48 // load_increment afterwards gives an error. 49 gboolean done; 50 51 // Image information. 52 size_t xsize; 53 size_t ysize; 54 gboolean alpha_premultiplied; 55 gboolean has_animation; 56 gboolean has_alpha; 57 uint64_t total_duration_ms; 58 uint64_t tick_duration_us; 59 uint64_t repetition_count; // 0 = loop forever 60 61 gchar *icc_base64; 62 }; 63 64 #define GDK_TYPE_PIXBUF_JXL_ANIMATION (gdk_pixbuf_jxl_animation_get_type()) 65 G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, GDK, 66 JXL_ANIMATION, GdkPixbufAnimation); 67 68 G_DEFINE_TYPE(GdkPixbufJxlAnimation, gdk_pixbuf_jxl_animation, 69 GDK_TYPE_PIXBUF_ANIMATION); 70 71 // Iterator to a given point in time in the animation; contains a pointer to the 72 // full animation. 73 struct _GdkPixbufJxlAnimationIter { 74 GdkPixbufAnimationIter parent_instance; 75 GdkPixbufJxlAnimation *animation; 76 size_t current_frame; 77 uint64_t time_offset; 78 }; 79 80 #define GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER \ 81 (gdk_pixbuf_jxl_animation_iter_get_type()) 82 G_DECLARE_FINAL_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter, 83 GDK, JXL_ANIMATION_ITER, GdkPixbufAnimationIter); 84 G_DEFINE_TYPE(GdkPixbufJxlAnimationIter, gdk_pixbuf_jxl_animation_iter, 85 GDK_TYPE_PIXBUF_ANIMATION_ITER); 86 87 static void gdk_pixbuf_jxl_animation_init(GdkPixbufJxlAnimation *obj) { 88 // Suppress "unused function" warnings. 89 (void)glib_autoptr_cleanup_GdkPixbufJxlAnimation; 90 (void)GDK_JXL_ANIMATION; 91 (void)GDK_IS_JXL_ANIMATION; 92 } 93 94 static gboolean gdk_pixbuf_jxl_animation_is_static_image( 95 GdkPixbufAnimation *anim) { 96 GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim; 97 return !jxl_anim->has_animation; 98 } 99 100 static GdkPixbuf *gdk_pixbuf_jxl_animation_get_static_image( 101 GdkPixbufAnimation *anim) { 102 GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim; 103 if (jxl_anim->frames == NULL || jxl_anim->frames->len == 0) return NULL; 104 GdkPixbufJxlAnimationFrame *frame = 105 &g_array_index(jxl_anim->frames, GdkPixbufJxlAnimationFrame, 0); 106 return frame->decoded ? frame->data : NULL; 107 } 108 109 static void gdk_pixbuf_jxl_animation_get_size(GdkPixbufAnimation *anim, 110 int *width, int *height) { 111 GdkPixbufJxlAnimation *jxl_anim = (GdkPixbufJxlAnimation *)anim; 112 if (width) *width = jxl_anim->xsize; 113 if (height) *height = jxl_anim->ysize; 114 } 115 116 G_GNUC_BEGIN_IGNORE_DEPRECATIONS 117 static gboolean gdk_pixbuf_jxl_animation_iter_advance( 118 GdkPixbufAnimationIter *iter, const GTimeVal *current_time); 119 120 static GdkPixbufAnimationIter *gdk_pixbuf_jxl_animation_get_iter( 121 GdkPixbufAnimation *anim, const GTimeVal *start_time) { 122 GdkPixbufJxlAnimationIter *iter = 123 g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION_ITER, NULL); 124 iter->animation = (GdkPixbufJxlAnimation *)anim; 125 iter->time_offset = start_time->tv_sec * 1000ULL + start_time->tv_usec / 1000; 126 g_object_ref(iter->animation); 127 gdk_pixbuf_jxl_animation_iter_advance((GdkPixbufAnimationIter *)iter, 128 start_time); 129 return (GdkPixbufAnimationIter *)iter; 130 } 131 G_GNUC_END_IGNORE_DEPRECATIONS 132 133 static void gdk_pixbuf_jxl_animation_finalize(GObject *obj) { 134 GdkPixbufJxlAnimation *decoder_state = (GdkPixbufJxlAnimation *)obj; 135 if (decoder_state->frames != NULL) { 136 for (size_t i = 0; i < decoder_state->frames->len; i++) { 137 g_object_unref( 138 g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, i) 139 .data); 140 } 141 g_array_free(decoder_state->frames, /*free_segment=*/TRUE); 142 } 143 JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner); 144 JxlDecoderDestroy(decoder_state->decoder); 145 g_free(decoder_state->icc_base64); 146 } 147 148 static void gdk_pixbuf_jxl_animation_class_init( 149 GdkPixbufJxlAnimationClass *klass) { 150 G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_finalize; 151 klass->parent_class.is_static_image = 152 gdk_pixbuf_jxl_animation_is_static_image; 153 klass->parent_class.get_static_image = 154 gdk_pixbuf_jxl_animation_get_static_image; 155 klass->parent_class.get_size = gdk_pixbuf_jxl_animation_get_size; 156 klass->parent_class.get_iter = gdk_pixbuf_jxl_animation_get_iter; 157 } 158 159 static void gdk_pixbuf_jxl_animation_iter_init(GdkPixbufJxlAnimationIter *obj) { 160 (void)glib_autoptr_cleanup_GdkPixbufJxlAnimationIter; 161 (void)GDK_JXL_ANIMATION_ITER; 162 (void)GDK_IS_JXL_ANIMATION_ITER; 163 } 164 165 static int gdk_pixbuf_jxl_animation_iter_get_delay_time( 166 GdkPixbufAnimationIter *iter) { 167 GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; 168 if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) { 169 return 0; 170 } 171 return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, 172 jxl_iter->current_frame) 173 .duration_ms; 174 } 175 176 static GdkPixbuf *gdk_pixbuf_jxl_animation_iter_get_pixbuf( 177 GdkPixbufAnimationIter *iter) { 178 GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; 179 if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) { 180 return NULL; 181 } 182 return g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, 183 jxl_iter->current_frame) 184 .data; 185 } 186 187 static gboolean gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame( 188 GdkPixbufAnimationIter *iter) { 189 GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; 190 if (jxl_iter->animation->frames->len <= jxl_iter->current_frame) { 191 return TRUE; 192 } 193 return !g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, 194 jxl_iter->current_frame) 195 .decoded; 196 } 197 198 G_GNUC_BEGIN_IGNORE_DEPRECATIONS 199 static gboolean gdk_pixbuf_jxl_animation_iter_advance( 200 GdkPixbufAnimationIter *iter, const GTimeVal *current_time) { 201 GdkPixbufJxlAnimationIter *jxl_iter = (GdkPixbufJxlAnimationIter *)iter; 202 size_t old_frame = jxl_iter->current_frame; 203 204 uint64_t current_time_ms = current_time->tv_sec * 1000ULL + 205 current_time->tv_usec / 1000 - 206 jxl_iter->time_offset; 207 208 if (jxl_iter->animation->frames->len == 0) { 209 jxl_iter->current_frame = 0; 210 } else if (!jxl_iter->animation->done && 211 current_time_ms >= jxl_iter->animation->total_duration_ms) { 212 jxl_iter->current_frame = jxl_iter->animation->frames->len - 1; 213 } else if (jxl_iter->animation->repetition_count != 0 && 214 current_time_ms > jxl_iter->animation->repetition_count * 215 jxl_iter->animation->total_duration_ms) { 216 jxl_iter->current_frame = jxl_iter->animation->frames->len - 1; 217 } else { 218 uint64_t total_duration_ms = jxl_iter->animation->total_duration_ms; 219 // Guard against divide-by-0 in malicious files. 220 if (total_duration_ms == 0) total_duration_ms = 1; 221 uint64_t loop_offset = current_time_ms % total_duration_ms; 222 jxl_iter->current_frame = 0; 223 while (TRUE) { 224 uint64_t duration = 225 g_array_index(jxl_iter->animation->frames, GdkPixbufJxlAnimationFrame, 226 jxl_iter->current_frame) 227 .duration_ms; 228 if (duration >= loop_offset) { 229 break; 230 } 231 loop_offset -= duration; 232 jxl_iter->current_frame++; 233 } 234 } 235 236 return old_frame != jxl_iter->current_frame; 237 } 238 G_GNUC_END_IGNORE_DEPRECATIONS 239 240 static void gdk_pixbuf_jxl_animation_iter_finalize(GObject *obj) { 241 GdkPixbufJxlAnimationIter *iter = (GdkPixbufJxlAnimationIter *)obj; 242 g_object_unref(iter->animation); 243 } 244 245 static void gdk_pixbuf_jxl_animation_iter_class_init( 246 GdkPixbufJxlAnimationIterClass *klass) { 247 G_OBJECT_CLASS(klass)->finalize = gdk_pixbuf_jxl_animation_iter_finalize; 248 klass->parent_class.get_delay_time = 249 gdk_pixbuf_jxl_animation_iter_get_delay_time; 250 klass->parent_class.get_pixbuf = gdk_pixbuf_jxl_animation_iter_get_pixbuf; 251 klass->parent_class.on_currently_loading_frame = 252 gdk_pixbuf_jxl_animation_iter_on_currently_loading_frame; 253 klass->parent_class.advance = gdk_pixbuf_jxl_animation_iter_advance; 254 } 255 256 G_END_DECLS 257 258 static gpointer begin_load(GdkPixbufModuleSizeFunc size_func, 259 GdkPixbufModulePreparedFunc prepare_func, 260 GdkPixbufModuleUpdatedFunc update_func, 261 gpointer user_data, GError **error) { 262 GdkPixbufJxlAnimation *decoder_state = 263 g_object_new(GDK_TYPE_PIXBUF_JXL_ANIMATION, NULL); 264 if (decoder_state == NULL) { 265 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 266 "Creation of the animation state failed"); 267 return NULL; 268 } 269 decoder_state->image_size_callback = size_func; 270 decoder_state->pixbuf_prepared_callback = prepare_func; 271 decoder_state->area_updated_callback = update_func; 272 decoder_state->user_data = user_data; 273 decoder_state->frames = 274 g_array_new(/*zero_terminated=*/FALSE, /*clear_=*/TRUE, 275 sizeof(GdkPixbufJxlAnimationFrame)); 276 277 if (decoder_state->frames == NULL) { 278 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 279 "Creation of the frame array failed"); 280 goto cleanup; 281 } 282 283 if (!(decoder_state->parallel_runner = 284 JxlResizableParallelRunnerCreate(NULL))) { 285 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 286 "Creation of the JXL parallel runner failed"); 287 goto cleanup; 288 } 289 290 if (!(decoder_state->decoder = JxlDecoderCreate(NULL))) { 291 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 292 "Creation of the JXL decoder failed"); 293 goto cleanup; 294 } 295 296 JxlDecoderStatus status; 297 298 if ((status = JxlDecoderSetParallelRunner( 299 decoder_state->decoder, JxlResizableParallelRunner, 300 decoder_state->parallel_runner)) != JXL_DEC_SUCCESS) { 301 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 302 "JxlDecoderSetParallelRunner failed: %x", status); 303 goto cleanup; 304 } 305 if ((status = JxlDecoderSubscribeEvents( 306 decoder_state->decoder, JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | 307 JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME)) != 308 JXL_DEC_SUCCESS) { 309 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 310 "JxlDecoderSubscribeEvents failed: %x", status); 311 goto cleanup; 312 } 313 314 decoder_state->pixel_format.data_type = JXL_TYPE_FLOAT; 315 decoder_state->pixel_format.endianness = JXL_NATIVE_ENDIAN; 316 317 return decoder_state; 318 cleanup: 319 JxlResizableParallelRunnerDestroy(decoder_state->parallel_runner); 320 JxlDecoderDestroy(decoder_state->decoder); 321 g_object_unref(decoder_state); 322 return NULL; 323 } 324 325 static gboolean stop_load(gpointer context, GError **error) { 326 g_object_unref(context); 327 return TRUE; 328 } 329 330 static gboolean load_increment(gpointer context, const guchar *buf, guint size, 331 GError **error) { 332 GdkPixbufJxlAnimation *decoder_state = context; 333 if (decoder_state->done == TRUE) { 334 g_warning_once("Trailing data found at end of JXL file"); 335 return TRUE; 336 } 337 338 JxlDecoderStatus status; 339 340 if ((status = JxlDecoderSetInput(decoder_state->decoder, buf, size)) != 341 JXL_DEC_SUCCESS) { 342 // Should never happen if things are done properly. 343 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 344 "JXL decoder logic error: %x", status); 345 return FALSE; 346 } 347 348 for (;;) { 349 status = JxlDecoderProcessInput(decoder_state->decoder); 350 switch (status) { 351 case JXL_DEC_NEED_MORE_INPUT: { 352 JxlDecoderReleaseInput(decoder_state->decoder); 353 return TRUE; 354 } 355 356 case JXL_DEC_BASIC_INFO: { 357 JxlBasicInfo info; 358 if (JxlDecoderGetBasicInfo(decoder_state->decoder, &info) != 359 JXL_DEC_SUCCESS) { 360 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 361 "JXLDecoderGetBasicInfo failed"); 362 return FALSE; 363 } 364 decoder_state->pixel_format.num_channels = info.alpha_bits > 0 ? 4 : 3; 365 decoder_state->alpha_premultiplied = info.alpha_premultiplied; 366 decoder_state->xsize = info.xsize; 367 decoder_state->ysize = info.ysize; 368 decoder_state->has_animation = info.have_animation; 369 decoder_state->has_alpha = info.alpha_bits > 0; 370 if (info.have_animation) { 371 decoder_state->repetition_count = info.animation.num_loops; 372 decoder_state->tick_duration_us = 1000000ULL * 373 info.animation.tps_denominator / 374 info.animation.tps_numerator; 375 } 376 gint width = info.xsize; 377 gint height = info.ysize; 378 if (decoder_state->image_size_callback) { 379 decoder_state->image_size_callback(&width, &height, 380 decoder_state->user_data); 381 } 382 383 // GDK convention for signaling being interested only in the basic info. 384 if (width == 0 || height == 0) { 385 decoder_state->done = TRUE; 386 return TRUE; 387 } 388 389 // Set an appropriate number of threads for the image size. 390 JxlResizableParallelRunnerSetThreads( 391 decoder_state->parallel_runner, 392 JxlResizableParallelRunnerSuggestThreads(info.xsize, info.ysize)); 393 break; 394 } 395 396 case JXL_DEC_COLOR_ENCODING: { 397 // Get the ICC color profile of the pixel data 398 gpointer icc_buff; 399 size_t icc_size; 400 JxlColorEncoding color_encoding; 401 if (JXL_DEC_SUCCESS == JxlDecoderGetColorAsEncodedProfile( 402 decoder_state->decoder, 403 JXL_COLOR_PROFILE_TARGET_ORIGINAL, 404 &color_encoding)) { 405 // we don't check the return status here because it's not a problem if 406 // this fails 407 JxlDecoderSetPreferredColorProfile(decoder_state->decoder, 408 &color_encoding); 409 } 410 if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize( 411 decoder_state->decoder, 412 JXL_COLOR_PROFILE_TARGET_DATA, &icc_size)) { 413 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 414 "JxlDecoderGetICCProfileSize failed"); 415 return FALSE; 416 } 417 if (!(icc_buff = g_malloc(icc_size))) { 418 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 419 "Allocating ICC profile failed"); 420 return FALSE; 421 } 422 if (JXL_DEC_SUCCESS != 423 JxlDecoderGetColorAsICCProfile(decoder_state->decoder, 424 JXL_COLOR_PROFILE_TARGET_DATA, 425 icc_buff, icc_size)) { 426 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 427 "JxlDecoderGetColorAsICCProfile failed"); 428 g_free(icc_buff); 429 return FALSE; 430 } 431 decoder_state->icc_base64 = g_base64_encode(icc_buff, icc_size); 432 g_free(icc_buff); 433 if (!decoder_state->icc_base64) { 434 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 435 "Allocating ICC profile base64 string failed"); 436 return FALSE; 437 } 438 439 break; 440 } 441 442 case JXL_DEC_FRAME: { 443 // TODO(veluca): support rescaling. 444 JxlFrameHeader frame_header; 445 if (JxlDecoderGetFrameHeader(decoder_state->decoder, &frame_header) != 446 JXL_DEC_SUCCESS) { 447 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 448 "Failed to retrieve frame info"); 449 return FALSE; 450 } 451 452 { 453 GdkPixbufJxlAnimationFrame frame; 454 frame.decoded = FALSE; 455 frame.duration_ms = 456 frame_header.duration * decoder_state->tick_duration_us / 1000; 457 decoder_state->total_duration_ms += frame.duration_ms; 458 frame.data = 459 gdk_pixbuf_new(GDK_COLORSPACE_RGB, decoder_state->has_alpha, 460 /*bits_per_sample=*/8, decoder_state->xsize, 461 decoder_state->ysize); 462 if (frame.data == NULL) { 463 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 464 "Failed to allocate output pixel buffer"); 465 return FALSE; 466 } 467 gdk_pixbuf_set_option(frame.data, "icc-profile", 468 decoder_state->icc_base64); 469 decoder_state->pixel_format.align = 470 gdk_pixbuf_get_rowstride(frame.data); 471 decoder_state->pixel_format.data_type = JXL_TYPE_UINT8; 472 g_array_append_val(decoder_state->frames, frame); 473 } 474 if (decoder_state->pixbuf_prepared_callback && 475 decoder_state->frames->len == 1) { 476 decoder_state->pixbuf_prepared_callback( 477 g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, 478 0) 479 .data, 480 decoder_state->has_animation ? (GdkPixbufAnimation *)decoder_state 481 : NULL, 482 decoder_state->user_data); 483 } 484 break; 485 } 486 487 case JXL_DEC_NEED_IMAGE_OUT_BUFFER: { 488 GdkPixbuf *output = 489 g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, 490 decoder_state->frames->len - 1) 491 .data; 492 decoder_state->pixel_format.align = gdk_pixbuf_get_rowstride(output); 493 guint size; 494 guchar *dst = gdk_pixbuf_get_pixels_with_length(output, &size); 495 if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer( 496 decoder_state->decoder, 497 &decoder_state->pixel_format, dst, size)) { 498 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 499 "JxlDecoderSetImageOutBuffer failed"); 500 return FALSE; 501 } 502 break; 503 } 504 505 case JXL_DEC_FULL_IMAGE: { 506 // TODO(veluca): consider doing partial updates. 507 if (decoder_state->area_updated_callback) { 508 GdkPixbuf *output = g_array_index(decoder_state->frames, 509 GdkPixbufJxlAnimationFrame, 0) 510 .data; 511 decoder_state->area_updated_callback( 512 output, 0, 0, gdk_pixbuf_get_width(output), 513 gdk_pixbuf_get_height(output), decoder_state->user_data); 514 } 515 g_array_index(decoder_state->frames, GdkPixbufJxlAnimationFrame, 516 decoder_state->frames->len - 1) 517 .decoded = TRUE; 518 break; 519 } 520 521 case JXL_DEC_SUCCESS: { 522 decoder_state->done = TRUE; 523 return TRUE; 524 } 525 526 default: { 527 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 528 "Unexpected JxlDecoderProcessInput return code: %x", 529 status); 530 return FALSE; 531 } 532 } 533 } 534 return TRUE; 535 } 536 537 static gboolean jxl_is_save_option_supported(const gchar *option_key) { 538 if (g_strcmp0(option_key, "quality") == 0) { 539 return TRUE; 540 } 541 542 return FALSE; 543 } 544 545 static gboolean jxl_image_saver(FILE *f, GdkPixbuf *pixbuf, gchar **keys, 546 gchar **values, GError **error) { 547 long quality = 90; /* default; must be between 0 and 100 */ 548 double distance; 549 gboolean save_alpha; 550 JxlEncoder *encoder; 551 void *parallel_runner; 552 JxlEncoderFrameSettings *frame_settings; 553 JxlBasicInfo output_info; 554 JxlPixelFormat pixel_format; 555 JxlColorEncoding color_profile; 556 JxlEncoderStatus status; 557 558 GByteArray *compressed; 559 size_t offset = 0; 560 uint8_t *next_out; 561 size_t avail_out; 562 563 if (f == NULL || pixbuf == NULL) { 564 return FALSE; 565 } 566 567 if (keys && *keys) { 568 gchar **kiter = keys; 569 gchar **viter = values; 570 571 while (*kiter) { 572 if (strcmp(*kiter, "quality") == 0) { 573 char *endptr = NULL; 574 quality = strtol(*viter, &endptr, 10); 575 576 if (endptr == *viter) { 577 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION, 578 "JXL quality must be a value between 0 and 100; value " 579 "\"%s\" could not be parsed.", 580 *viter); 581 582 return FALSE; 583 } 584 585 if (quality < 0 || quality > 100) { 586 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_BAD_OPTION, 587 "JXL quality must be a value between 0 and 100; value " 588 "\"%ld\" is not allowed.", 589 quality); 590 591 return FALSE; 592 } 593 } else { 594 g_warning("Unrecognized parameter (%s) passed to JXL saver.", *kiter); 595 } 596 597 ++kiter; 598 ++viter; 599 } 600 } 601 602 if (gdk_pixbuf_get_bits_per_sample(pixbuf) != 8) { 603 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, 604 "Sorry, only 8bit images are supported by this JXL saver"); 605 return FALSE; 606 } 607 608 JxlEncoderInitBasicInfo(&output_info); 609 output_info.have_container = JXL_FALSE; 610 output_info.xsize = gdk_pixbuf_get_width(pixbuf); 611 output_info.ysize = gdk_pixbuf_get_height(pixbuf); 612 output_info.bits_per_sample = 8; 613 output_info.orientation = JXL_ORIENT_IDENTITY; 614 output_info.num_color_channels = 3; 615 616 if (output_info.xsize == 0 || output_info.ysize == 0) { 617 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_CORRUPT_IMAGE, 618 "Empty image, nothing to save"); 619 return FALSE; 620 } 621 622 save_alpha = gdk_pixbuf_get_has_alpha(pixbuf); 623 624 pixel_format.data_type = JXL_TYPE_UINT8; 625 pixel_format.endianness = JXL_NATIVE_ENDIAN; 626 pixel_format.align = gdk_pixbuf_get_rowstride(pixbuf); 627 628 if (save_alpha) { 629 if (gdk_pixbuf_get_n_channels(pixbuf) != 4) { 630 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, 631 "Unsupported number of channels"); 632 return FALSE; 633 } 634 635 output_info.num_extra_channels = 1; 636 output_info.alpha_bits = 8; 637 pixel_format.num_channels = 4; 638 } else { 639 if (gdk_pixbuf_get_n_channels(pixbuf) != 3) { 640 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_UNKNOWN_TYPE, 641 "Unsupported number of channels"); 642 return FALSE; 643 } 644 645 output_info.num_extra_channels = 0; 646 output_info.alpha_bits = 0; 647 pixel_format.num_channels = 3; 648 } 649 650 encoder = JxlEncoderCreate(NULL); 651 if (!encoder) { 652 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 653 "Creation of the JXL encoder failed"); 654 return FALSE; 655 } 656 657 parallel_runner = JxlResizableParallelRunnerCreate(NULL); 658 if (!parallel_runner) { 659 JxlEncoderDestroy(encoder); 660 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 661 "Creation of the JXL decoder failed"); 662 return FALSE; 663 } 664 665 JxlResizableParallelRunnerSetThreads( 666 parallel_runner, JxlResizableParallelRunnerSuggestThreads( 667 output_info.xsize, output_info.ysize)); 668 669 status = JxlEncoderSetParallelRunner(encoder, JxlResizableParallelRunner, 670 parallel_runner); 671 if (status != JXL_ENC_SUCCESS) { 672 JxlResizableParallelRunnerDestroy(parallel_runner); 673 JxlEncoderDestroy(encoder); 674 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 675 "JxlDecoderSetParallelRunner failed: %x", status); 676 return FALSE; 677 } 678 679 if (quality > 99) { 680 output_info.uses_original_profile = JXL_TRUE; 681 distance = 0; 682 } else { 683 output_info.uses_original_profile = JXL_FALSE; 684 distance = JxlEncoderDistanceFromQuality((float)quality); 685 } 686 687 status = JxlEncoderSetBasicInfo(encoder, &output_info); 688 if (status != JXL_ENC_SUCCESS) { 689 JxlResizableParallelRunnerDestroy(parallel_runner); 690 JxlEncoderDestroy(encoder); 691 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 692 "JxlEncoderSetBasicInfo failed: %x", status); 693 return FALSE; 694 } 695 696 JxlColorEncodingSetToSRGB(&color_profile, JXL_FALSE); 697 status = JxlEncoderSetColorEncoding(encoder, &color_profile); 698 if (status != JXL_ENC_SUCCESS) { 699 JxlResizableParallelRunnerDestroy(parallel_runner); 700 JxlEncoderDestroy(encoder); 701 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 702 "JxlEncoderSetColorEncoding failed: %x", status); 703 return FALSE; 704 } 705 706 frame_settings = JxlEncoderFrameSettingsCreate(encoder, NULL); 707 JxlEncoderSetFrameDistance(frame_settings, distance); 708 JxlEncoderSetFrameLossless(frame_settings, output_info.uses_original_profile); 709 710 status = JxlEncoderAddImageFrame(frame_settings, &pixel_format, 711 gdk_pixbuf_read_pixels(pixbuf), 712 gdk_pixbuf_get_byte_length(pixbuf)); 713 if (status != JXL_ENC_SUCCESS) { 714 JxlResizableParallelRunnerDestroy(parallel_runner); 715 JxlEncoderDestroy(encoder); 716 g_set_error(error, GDK_PIXBUF_ERROR, GDK_PIXBUF_ERROR_FAILED, 717 "JxlEncoderAddImageFrame failed: %x", status); 718 return FALSE; 719 } 720 721 JxlEncoderCloseInput(encoder); 722 723 compressed = g_byte_array_sized_new(4096); 724 g_byte_array_set_size(compressed, 4096); 725 do { 726 next_out = compressed->data + offset; 727 avail_out = compressed->len - offset; 728 status = JxlEncoderProcessOutput(encoder, &next_out, &avail_out); 729 730 if (status == JXL_ENC_NEED_MORE_OUTPUT) { 731 offset = next_out - compressed->data; 732 g_byte_array_set_size(compressed, compressed->len * 2); 733 } else if (status == JXL_ENC_ERROR) { 734 JxlResizableParallelRunnerDestroy(parallel_runner); 735 JxlEncoderDestroy(encoder); 736 g_set_error(error, G_FILE_ERROR, 0, "JxlEncoderProcessOutput failed: %x", 737 status); 738 return FALSE; 739 } 740 } while (status != JXL_ENC_SUCCESS); 741 742 JxlResizableParallelRunnerDestroy(parallel_runner); 743 JxlEncoderDestroy(encoder); 744 745 g_byte_array_set_size(compressed, next_out - compressed->data); 746 if (compressed->len > 0) { 747 fwrite(compressed->data, 1, compressed->len, f); 748 g_byte_array_free(compressed, TRUE); 749 return TRUE; 750 } 751 752 return FALSE; 753 } 754 755 void fill_vtable(GdkPixbufModule *module) { 756 module->begin_load = begin_load; 757 module->stop_load = stop_load; 758 module->load_increment = load_increment; 759 module->is_save_option_supported = jxl_is_save_option_supported; 760 module->save = jxl_image_saver; 761 } 762 763 void fill_info(GdkPixbufFormat *info) { 764 static GdkPixbufModulePattern signature[] = { 765 {"\xFF\x0A", " ", 100}, 766 {"...\x0CJXL \x0D\x0A\x87\x0A", "zzz ", 100}, 767 {NULL, NULL, 0}, 768 }; 769 770 static gchar *mime_types[] = {"image/jxl", NULL}; 771 772 static gchar *extensions[] = {"jxl", NULL}; 773 774 info->name = "jxl"; 775 info->signature = signature; 776 info->description = "JPEG XL image"; 777 info->mime_types = mime_types; 778 info->extensions = extensions; 779 info->flags = GDK_PIXBUF_FORMAT_WRITABLE | GDK_PIXBUF_FORMAT_THREADSAFE; 780 info->license = "BSD-3"; 781 }