file-jxl-load.cc (17661B)
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 "plugins/gimp/file-jxl-load.h" 7 8 #include <jxl/decode.h> 9 #include <jxl/decode_cxx.h> 10 11 #define _PROFILE_ORIGIN_ JXL_COLOR_PROFILE_TARGET_ORIGINAL 12 #define _PROFILE_TARGET_ JXL_COLOR_PROFILE_TARGET_DATA 13 #define LOAD_PROC "file-jxl-load" 14 15 namespace jxl { 16 17 bool SetJpegXlOutBuffer( 18 std::unique_ptr<JxlDecoderStruct, JxlDecoderDestroyStruct> *dec, 19 JxlPixelFormat *format, size_t *buffer_size, gpointer *pixels_buffer_1) { 20 if (JXL_DEC_SUCCESS != 21 JxlDecoderImageOutBufferSize(dec->get(), format, buffer_size)) { 22 g_printerr(LOAD_PROC " Error: JxlDecoderImageOutBufferSize failed\n"); 23 return false; 24 } 25 *pixels_buffer_1 = g_malloc(*buffer_size); 26 if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec->get(), format, 27 *pixels_buffer_1, 28 *buffer_size)) { 29 g_printerr(LOAD_PROC " Error: JxlDecoderSetImageOutBuffer failed\n"); 30 return false; 31 } 32 return true; 33 } 34 35 bool LoadJpegXlImage(const gchar *const filename, gint32 *const image_id) { 36 bool stop_processing = false; 37 JxlDecoderStatus status = JXL_DEC_NEED_MORE_INPUT; 38 std::vector<uint8_t> icc_profile; 39 GimpColorProfile *profile_icc = nullptr; 40 GimpColorProfile *profile_int = nullptr; 41 bool is_linear = false; 42 uint32_t xsize = 0; 43 uint32_t ysize = 0; 44 int32_t crop_x0 = 0; 45 int32_t crop_y0 = 0; 46 size_t layer_idx = 0; 47 uint32_t frame_duration = 0; 48 double tps_denom = 1.f; 49 double tps_numerator = 1.f; 50 51 gint32 layer; 52 53 gpointer pixels_buffer_1 = nullptr; 54 gpointer pixels_buffer_2 = nullptr; 55 size_t buffer_size = 0; 56 57 GimpImageBaseType image_type = GIMP_RGB; 58 GimpImageType layer_type = GIMP_RGB_IMAGE; 59 GimpPrecision precision = GIMP_PRECISION_U16_GAMMA; 60 JxlBasicInfo info = {}; 61 JxlPixelFormat format = {}; 62 JxlAnimationHeader animation = {}; 63 JxlBlendMode blend_mode = JXL_BLEND_BLEND; 64 std::vector<char> frame_name; 65 66 format.num_channels = 4; 67 format.data_type = JXL_TYPE_FLOAT; 68 format.endianness = JXL_NATIVE_ENDIAN; 69 format.align = 0; 70 71 bool is_gray = false; 72 73 JpegXlGimpProgress gimp_load_progress( 74 ("Opening JPEG XL file:" + std::string(filename)).c_str()); 75 gimp_load_progress.update(); 76 77 // read file 78 std::ifstream instream(filename, std::ios::in | std::ios::binary); 79 std::vector<uint8_t> compressed((std::istreambuf_iterator<char>(instream)), 80 std::istreambuf_iterator<char>()); 81 instream.close(); 82 83 gimp_load_progress.update(); 84 85 // multi-threaded parallel runner. 86 auto runner = JxlResizableParallelRunnerMake(nullptr); 87 88 auto dec = JxlDecoderMake(nullptr); 89 if (JXL_DEC_SUCCESS != 90 JxlDecoderSubscribeEvents( 91 dec.get(), JXL_DEC_BASIC_INFO | JXL_DEC_COLOR_ENCODING | 92 JXL_DEC_FULL_IMAGE | JXL_DEC_FRAME_PROGRESSION | 93 JXL_DEC_FRAME)) { 94 g_printerr(LOAD_PROC " Error: JxlDecoderSubscribeEvents failed\n"); 95 return false; 96 } 97 98 if (JXL_DEC_SUCCESS != JxlDecoderSetParallelRunner(dec.get(), 99 JxlResizableParallelRunner, 100 runner.get())) { 101 g_printerr(LOAD_PROC " Error: JxlDecoderSetParallelRunner failed\n"); 102 return false; 103 } 104 // TODO(user): make this work with coalescing set to false, while handling 105 // frames with duration 0 and references to earlier frames correctly. 106 if (JXL_DEC_SUCCESS != JxlDecoderSetCoalescing(dec.get(), JXL_TRUE)) { 107 g_printerr(LOAD_PROC " Error: JxlDecoderSetCoalescing failed\n"); 108 return false; 109 } 110 111 // grand decode loop... 112 JxlDecoderSetInput(dec.get(), compressed.data(), compressed.size()); 113 114 if (JXL_DEC_SUCCESS != JxlDecoderSetProgressiveDetail( 115 dec.get(), JxlProgressiveDetail::kPasses)) { 116 g_printerr(LOAD_PROC " Error: JxlDecoderSetProgressiveDetail failed\n"); 117 return false; 118 } 119 120 while (true) { 121 gimp_load_progress.update(); 122 123 if (!stop_processing) status = JxlDecoderProcessInput(dec.get()); 124 125 if (status == JXL_DEC_BASIC_INFO) { 126 if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec.get(), &info)) { 127 g_printerr(LOAD_PROC " Error: JxlDecoderGetBasicInfo failed\n"); 128 return false; 129 } 130 131 xsize = info.xsize; 132 ysize = info.ysize; 133 if (info.have_animation) { 134 animation = info.animation; 135 tps_denom = animation.tps_denominator; 136 tps_numerator = animation.tps_numerator; 137 } 138 139 JxlResizableParallelRunnerSetThreads( 140 runner.get(), JxlResizableParallelRunnerSuggestThreads(xsize, ysize)); 141 } else if (status == JXL_DEC_COLOR_ENCODING) { 142 // check for ICC profile 143 size_t icc_size = 0; 144 JxlColorEncoding color_encoding; 145 if (JXL_DEC_SUCCESS != 146 JxlDecoderGetColorAsEncodedProfile(dec.get(), _PROFILE_ORIGIN_, 147 &color_encoding)) { 148 // Attempt to load ICC profile when no internal color encoding 149 if (JXL_DEC_SUCCESS != JxlDecoderGetICCProfileSize( 150 dec.get(), _PROFILE_ORIGIN_, &icc_size)) { 151 g_printerr(LOAD_PROC 152 " Warning: JxlDecoderGetICCProfileSize failed\n"); 153 } 154 155 if (icc_size > 0) { 156 icc_profile.resize(icc_size); 157 if (JXL_DEC_SUCCESS != JxlDecoderGetColorAsICCProfile( 158 dec.get(), _PROFILE_ORIGIN_, 159 icc_profile.data(), icc_profile.size())) { 160 g_printerr(LOAD_PROC 161 " Warning: JxlDecoderGetColorAsICCProfile failed\n"); 162 } 163 164 profile_icc = gimp_color_profile_new_from_icc_profile( 165 icc_profile.data(), icc_profile.size(), nullptr); 166 167 if (profile_icc) { 168 is_linear = gimp_color_profile_is_linear(profile_icc); 169 g_printerr(LOAD_PROC " Info: Color profile is_linear = %d\n", 170 is_linear); 171 } else { 172 g_printerr(LOAD_PROC " Warning: Failed to read ICC profile.\n"); 173 } 174 } else { 175 g_printerr(LOAD_PROC " Warning: Empty ICC data.\n"); 176 } 177 } 178 179 // Internal color profile detection... 180 if (JXL_DEC_SUCCESS == 181 JxlDecoderGetColorAsEncodedProfile(dec.get(), _PROFILE_TARGET_, 182 &color_encoding)) { 183 g_printerr(LOAD_PROC " Info: Internal color encoding detected.\n"); 184 185 // figure out linearity of internal profile 186 switch (color_encoding.transfer_function) { 187 case JXL_TRANSFER_FUNCTION_LINEAR: 188 is_linear = true; 189 break; 190 191 case JXL_TRANSFER_FUNCTION_709: 192 case JXL_TRANSFER_FUNCTION_PQ: 193 case JXL_TRANSFER_FUNCTION_HLG: 194 case JXL_TRANSFER_FUNCTION_GAMMA: 195 case JXL_TRANSFER_FUNCTION_DCI: 196 case JXL_TRANSFER_FUNCTION_SRGB: 197 is_linear = false; 198 break; 199 200 case JXL_TRANSFER_FUNCTION_UNKNOWN: 201 default: 202 if (profile_icc) { 203 g_printerr(LOAD_PROC 204 " Info: Unknown transfer function. " 205 "ICC profile is present."); 206 } else { 207 g_printerr(LOAD_PROC 208 " Info: Unknown transfer function. " 209 "No ICC profile present."); 210 } 211 break; 212 } 213 214 switch (color_encoding.color_space) { 215 case JXL_COLOR_SPACE_RGB: 216 if (color_encoding.white_point == JXL_WHITE_POINT_D65 && 217 color_encoding.primaries == JXL_PRIMARIES_SRGB) { 218 if (is_linear) { 219 profile_int = gimp_color_profile_new_rgb_srgb_linear(); 220 } else { 221 profile_int = gimp_color_profile_new_rgb_srgb(); 222 } 223 } else if (!is_linear && 224 color_encoding.white_point == JXL_WHITE_POINT_D65 && 225 (color_encoding.primaries_green_xy[0] == 0.2100 || 226 color_encoding.primaries_green_xy[1] == 0.7100)) { 227 // Probably Adobe RGB 228 profile_int = gimp_color_profile_new_rgb_adobe(); 229 } else if (profile_icc) { 230 g_printerr(LOAD_PROC 231 " Info: Unknown RGB colorspace. " 232 "Using ICC profile.\n"); 233 } else { 234 g_printerr(LOAD_PROC 235 " Info: Unknown RGB colorspace. " 236 "Treating as sRGB.\n"); 237 if (is_linear) { 238 profile_int = gimp_color_profile_new_rgb_srgb_linear(); 239 } else { 240 profile_int = gimp_color_profile_new_rgb_srgb(); 241 } 242 } 243 break; 244 245 case JXL_COLOR_SPACE_GRAY: 246 is_gray = true; 247 if (!profile_icc || 248 color_encoding.white_point == JXL_WHITE_POINT_D65) { 249 if (is_linear) { 250 profile_int = gimp_color_profile_new_d65_gray_linear(); 251 } else { 252 profile_int = gimp_color_profile_new_d65_gray_srgb_trc(); 253 } 254 } 255 break; 256 case JXL_COLOR_SPACE_XYB: 257 case JXL_COLOR_SPACE_UNKNOWN: 258 default: 259 if (profile_icc) { 260 g_printerr(LOAD_PROC 261 " Info: Unknown colorspace. Using ICC profile.\n"); 262 } else { 263 g_error( 264 LOAD_PROC 265 " Warning: Unknown colorspace. Treating as sRGB profile.\n"); 266 267 if (is_linear) { 268 profile_int = gimp_color_profile_new_rgb_srgb_linear(); 269 } else { 270 profile_int = gimp_color_profile_new_rgb_srgb(); 271 } 272 } 273 break; 274 } 275 } 276 277 // set pixel format 278 if (info.num_color_channels > 1) { 279 if (info.alpha_bits == 0) { 280 image_type = GIMP_RGB; 281 layer_type = GIMP_RGB_IMAGE; 282 format.num_channels = info.num_color_channels; 283 } else { 284 image_type = GIMP_RGB; 285 layer_type = GIMP_RGBA_IMAGE; 286 format.num_channels = info.num_color_channels + 1; 287 } 288 } else if (info.num_color_channels == 1) { 289 if (info.alpha_bits == 0) { 290 image_type = GIMP_GRAY; 291 layer_type = GIMP_GRAY_IMAGE; 292 format.num_channels = info.num_color_channels; 293 } else { 294 image_type = GIMP_GRAY; 295 layer_type = GIMP_GRAYA_IMAGE; 296 format.num_channels = info.num_color_channels + 1; 297 } 298 } 299 300 // Set image bit depth and linearity 301 if (info.bits_per_sample <= 8) { 302 if (is_linear) { 303 precision = GIMP_PRECISION_U8_LINEAR; 304 } else { 305 precision = GIMP_PRECISION_U8_GAMMA; 306 } 307 } else if (info.bits_per_sample <= 16) { 308 if (info.exponent_bits_per_sample > 0) { 309 if (is_linear) { 310 precision = GIMP_PRECISION_HALF_LINEAR; 311 } else { 312 precision = GIMP_PRECISION_HALF_GAMMA; 313 } 314 } else if (is_linear) { 315 precision = GIMP_PRECISION_U16_LINEAR; 316 } else { 317 precision = GIMP_PRECISION_U16_GAMMA; 318 } 319 } else { 320 if (info.exponent_bits_per_sample > 0) { 321 if (is_linear) { 322 precision = GIMP_PRECISION_FLOAT_LINEAR; 323 } else { 324 precision = GIMP_PRECISION_FLOAT_GAMMA; 325 } 326 } else if (is_linear) { 327 precision = GIMP_PRECISION_U32_LINEAR; 328 } else { 329 precision = GIMP_PRECISION_U32_GAMMA; 330 } 331 } 332 333 // create new image 334 if (is_linear) { 335 *image_id = gimp_image_new_with_precision(xsize, ysize, image_type, 336 GIMP_PRECISION_FLOAT_LINEAR); 337 } else { 338 *image_id = gimp_image_new_with_precision(xsize, ysize, image_type, 339 GIMP_PRECISION_FLOAT_GAMMA); 340 } 341 342 if (profile_int) { 343 gimp_image_set_color_profile(*image_id, profile_int); 344 } else if (!profile_icc) { 345 g_printerr(LOAD_PROC " Warning: No color profile.\n"); 346 } 347 } else if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { 348 // get image from decoder in FLOAT 349 format.data_type = JXL_TYPE_FLOAT; 350 if (!SetJpegXlOutBuffer(&dec, &format, &buffer_size, &pixels_buffer_1)) 351 return false; 352 } else if (status == JXL_DEC_FULL_IMAGE) { 353 // create and insert layer 354 gchar *layer_name; 355 if (layer_idx == 0 && !info.have_animation) { 356 layer_name = g_strdup_printf("Background"); 357 } else { 358 const char *blend = (blend_mode == JXL_BLEND_REPLACE) ? " (replace)" 359 : (blend_mode == JXL_BLEND_BLEND) ? " (combine)" 360 : ""; 361 char *temp_frame_name = nullptr; 362 bool must_free_frame_name = false; 363 if (frame_name.size() == 0) { 364 temp_frame_name = g_strdup_printf("Frame %lu", layer_idx + 1); 365 must_free_frame_name = true; 366 } else { 367 temp_frame_name = frame_name.data(); 368 } 369 double fduration = frame_duration * 1000.f * tps_denom / tps_numerator; 370 layer_name = g_strdup_printf("%s (%.15gms)%s", temp_frame_name, 371 fduration, blend); 372 if (must_free_frame_name) free(temp_frame_name); 373 } 374 layer = gimp_layer_new(*image_id, layer_name, xsize, ysize, layer_type, 375 /*opacity=*/100, 376 gimp_image_get_default_new_layer_mode(*image_id)); 377 378 gimp_image_insert_layer(*image_id, layer, /*parent_id=*/-1, 379 /*position=*/0); 380 381 pixels_buffer_2 = g_malloc(buffer_size); 382 GeglBuffer *buffer = gimp_drawable_get_buffer(layer); 383 const Babl *destination_format = gegl_buffer_set_format(buffer, nullptr); 384 385 std::string babl_format_str = ""; 386 if (is_gray) { 387 babl_format_str += "Y'"; 388 } else { 389 babl_format_str += "R'G'B'"; 390 } 391 if (info.alpha_bits > 0) { 392 babl_format_str += "A"; 393 } 394 babl_format_str += " float"; 395 396 const Babl *source_format = babl_format(babl_format_str.c_str()); 397 398 babl_process(babl_fish(source_format, destination_format), 399 pixels_buffer_1, pixels_buffer_2, xsize * ysize); 400 401 gegl_buffer_set(buffer, GEGL_RECTANGLE(0, 0, xsize, ysize), 0, nullptr, 402 pixels_buffer_2, GEGL_AUTO_ROWSTRIDE); 403 gimp_item_transform_translate(layer, crop_x0, crop_y0); 404 405 g_clear_object(&buffer); 406 g_free(pixels_buffer_1); 407 g_free(pixels_buffer_2); 408 if (stop_processing) status = JXL_DEC_SUCCESS; 409 g_free(layer_name); 410 layer_idx++; 411 } else if (status == JXL_DEC_FRAME) { 412 JxlFrameHeader frame_header; 413 if (JxlDecoderGetFrameHeader(dec.get(), &frame_header) != 414 JXL_DEC_SUCCESS) { 415 g_printerr(LOAD_PROC " Error: JxlDecoderSetImageOutBuffer failed\n"); 416 return false; 417 } 418 xsize = frame_header.layer_info.xsize; 419 ysize = frame_header.layer_info.ysize; 420 crop_x0 = frame_header.layer_info.crop_x0; 421 crop_y0 = frame_header.layer_info.crop_y0; 422 frame_duration = frame_header.duration; 423 blend_mode = frame_header.layer_info.blend_info.blendmode; 424 if (blend_mode != JXL_BLEND_BLEND && blend_mode != JXL_BLEND_REPLACE) { 425 g_printerr( 426 LOAD_PROC 427 " Warning: JxlDecoderGetFrameHeader: Unhandled blend mode: %d\n", 428 blend_mode); 429 } 430 if (frame_header.name_length > 0) { 431 frame_name.resize(frame_header.name_length + 1); 432 if (JXL_DEC_SUCCESS != JxlDecoderGetFrameName(dec.get(), 433 frame_name.data(), 434 frame_name.size())) { 435 g_printerr(LOAD_PROC "Error: JxlDecoderGetFrameName failed"); 436 return false; 437 } 438 } else { 439 frame_name.resize(0); 440 } 441 } else if (status == JXL_DEC_SUCCESS) { 442 // All decoding successfully finished. 443 // It's not required to call JxlDecoderReleaseInput(dec.get()) 444 // since the decoder will be destroyed. 445 break; 446 } else if (status == JXL_DEC_NEED_MORE_INPUT || 447 status == JXL_DEC_FRAME_PROGRESSION) { 448 stop_processing = status != JXL_DEC_FRAME_PROGRESSION; 449 if (JxlDecoderFlushImage(dec.get()) == JXL_DEC_SUCCESS) { 450 status = JXL_DEC_FULL_IMAGE; 451 continue; 452 } 453 g_printerr(LOAD_PROC " Error: Already provided all input\n"); 454 return false; 455 } else if (status == JXL_DEC_ERROR) { 456 g_printerr(LOAD_PROC " Error: Decoder error\n"); 457 return false; 458 } else { 459 g_printerr(LOAD_PROC " Error: Unknown decoder status\n"); 460 return false; 461 } 462 } // end grand decode loop 463 464 gimp_load_progress.update(); 465 466 if (profile_icc) { 467 gimp_image_set_color_profile(*image_id, profile_icc); 468 } 469 470 gimp_load_progress.update(); 471 472 // TODO(xiota): Add option to keep image as float 473 if (info.bits_per_sample < 32) { 474 gimp_image_convert_precision(*image_id, precision); 475 } 476 477 gimp_image_set_filename(*image_id, filename); 478 479 gimp_load_progress.finished(); 480 return true; 481 } 482 483 } // namespace jxl