file-jxl-save.cc (30036B)
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-save.h" 7 8 #include <jxl/encode.h> 9 #include <jxl/encode_cxx.h> 10 #include <jxl/types.h> 11 12 #include <cmath> 13 #include <utility> 14 15 #include "gobject/gsignal.h" 16 17 #define PLUG_IN_BINARY "file-jxl" 18 #define SAVE_PROC "file-jxl-save" 19 20 #define SCALE_WIDTH 200 21 22 namespace jxl { 23 24 namespace { 25 26 #ifndef g_clear_signal_handler 27 // g_clear_signal_handler was added in glib 2.62 28 void g_clear_signal_handler(gulong* handler, gpointer instance) { 29 if (handler != nullptr && *handler != 0) { 30 g_signal_handler_disconnect(instance, *handler); 31 *handler = 0; 32 } 33 } 34 #endif // g_clear_signal_handler 35 36 class JpegXlSaveOpts { 37 public: 38 float distance; 39 float quality; 40 41 bool lossless = false; 42 bool is_linear = false; 43 bool has_alpha = false; 44 bool is_gray = false; 45 bool icc_attached = false; 46 47 bool advanced_mode = false; 48 bool use_container = true; 49 bool save_exif = false; 50 int encoding_effort = 7; 51 int faster_decoding = 0; 52 53 std::string babl_format_str = "RGB u16"; 54 std::string babl_type_str = "u16"; 55 std::string babl_model_str = "RGB"; 56 57 JxlPixelFormat pixel_format; 58 JxlBasicInfo basic_info; 59 60 // functions 61 JpegXlSaveOpts(); 62 63 bool SetDistance(float dist); 64 bool SetQuality(float qual); 65 bool SetDimensions(int x, int y); 66 bool SetNumChannels(int channels); 67 68 bool UpdateDistance(); 69 bool UpdateQuality(); 70 71 bool SetModel(bool is_linear_); 72 73 bool UpdateBablFormat(); 74 bool SetBablModel(std::string model); 75 bool SetBablType(std::string type); 76 77 bool SetPrecision(int gimp_precision); 78 79 private: 80 }; // class JpegXlSaveOpts 81 82 JpegXlSaveOpts jxl_save_opts; 83 84 class JpegXlSaveGui { 85 public: 86 bool SaveDialog(); 87 88 private: 89 GtkWidget* toggle_lossless = nullptr; 90 GtkAdjustment* entry_distance = nullptr; 91 GtkAdjustment* entry_quality = nullptr; 92 GtkAdjustment* entry_effort = nullptr; 93 GtkAdjustment* entry_faster = nullptr; 94 GtkWidget* frame_advanced = nullptr; 95 GtkWidget* toggle_no_xyb = nullptr; 96 GtkWidget* toggle_raw = nullptr; 97 gulong handle_toggle_lossless = 0; 98 gulong handle_entry_quality = 0; 99 gulong handle_entry_distance = 0; 100 101 static bool GuiOnChangeQuality(GtkAdjustment* adj_qual, void* this_pointer); 102 103 static bool GuiOnChangeDistance(GtkAdjustment* adj_dist, void* this_pointer); 104 105 static bool GuiOnChangeEffort(GtkAdjustment* adj_effort); 106 static bool GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer); 107 static bool GuiOnChangeCodestream(GtkWidget* toggle); 108 static bool GuiOnChangeNoXYB(GtkWidget* toggle); 109 110 static bool GuiOnChangeAdvancedMode(GtkWidget* toggle, void* this_pointer); 111 }; // class JpegXlSaveGui 112 113 JpegXlSaveGui jxl_save_gui; 114 115 bool JpegXlSaveGui::GuiOnChangeQuality(GtkAdjustment* adj_qual, 116 void* this_pointer) { 117 JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer); 118 119 g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance); 120 g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality); 121 g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless); 122 123 GtkAdjustment* adj_dist = self->entry_distance; 124 jxl_save_opts.SetQuality(gtk_adjustment_get_value(adj_qual)); 125 gtk_adjustment_set_value(adj_dist, jxl_save_opts.distance); 126 127 self->handle_toggle_lossless = g_signal_connect( 128 self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self); 129 self->handle_entry_distance = 130 g_signal_connect(self->entry_distance, "value-changed", 131 G_CALLBACK(GuiOnChangeDistance), self); 132 self->handle_entry_quality = 133 g_signal_connect(self->entry_quality, "value-changed", 134 G_CALLBACK(GuiOnChangeQuality), self); 135 return true; 136 } 137 138 bool JpegXlSaveGui::GuiOnChangeDistance(GtkAdjustment* adj_dist, 139 void* this_pointer) { 140 JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer); 141 GtkAdjustment* adj_qual = self->entry_quality; 142 143 g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance); 144 g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality); 145 g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless); 146 147 jxl_save_opts.SetDistance(gtk_adjustment_get_value(adj_dist)); 148 gtk_adjustment_set_value(adj_qual, jxl_save_opts.quality); 149 150 if (!(jxl_save_opts.distance < 0.001)) { 151 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_lossless), 152 false); 153 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false); 154 } 155 156 self->handle_toggle_lossless = g_signal_connect( 157 self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self); 158 self->handle_entry_distance = 159 g_signal_connect(self->entry_distance, "value-changed", 160 G_CALLBACK(GuiOnChangeDistance), self); 161 self->handle_entry_quality = 162 g_signal_connect(self->entry_quality, "value-changed", 163 G_CALLBACK(GuiOnChangeQuality), self); 164 return true; 165 } 166 167 bool JpegXlSaveGui::GuiOnChangeEffort(GtkAdjustment* adj_effort) { 168 float new_effort = 10 - gtk_adjustment_get_value(adj_effort); 169 jxl_save_opts.encoding_effort = new_effort; 170 return true; 171 } 172 173 bool JpegXlSaveGui::GuiOnChangeLossless(GtkWidget* toggle, void* this_pointer) { 174 JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer); 175 GtkAdjustment* adj_distance = self->entry_distance; 176 GtkAdjustment* adj_quality = self->entry_quality; 177 GtkAdjustment* adj_effort = self->entry_effort; 178 179 jxl_save_opts.lossless = 180 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); 181 182 g_clear_signal_handler(&self->handle_entry_distance, self->entry_distance); 183 g_clear_signal_handler(&self->handle_entry_quality, self->entry_quality); 184 g_clear_signal_handler(&self->handle_toggle_lossless, self->toggle_lossless); 185 186 if (jxl_save_opts.lossless) { 187 gtk_adjustment_set_value(adj_quality, 100.0); 188 gtk_adjustment_set_value(adj_distance, 0.0); 189 jxl_save_opts.distance = 0; 190 jxl_save_opts.UpdateQuality(); 191 gtk_adjustment_set_value(adj_effort, 7); 192 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), true); 193 } else { 194 gtk_adjustment_set_value(adj_quality, 90.0); 195 gtk_adjustment_set_value(adj_distance, 1.0); 196 jxl_save_opts.distance = 1.0; 197 jxl_save_opts.UpdateQuality(); 198 gtk_adjustment_set_value(adj_effort, 3); 199 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false); 200 } 201 self->handle_toggle_lossless = g_signal_connect( 202 self->toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), self); 203 self->handle_entry_distance = 204 g_signal_connect(self->entry_distance, "value-changed", 205 G_CALLBACK(GuiOnChangeDistance), self); 206 self->handle_entry_quality = 207 g_signal_connect(self->entry_quality, "value-changed", 208 G_CALLBACK(GuiOnChangeQuality), self); 209 return true; 210 } 211 212 bool JpegXlSaveGui::GuiOnChangeCodestream(GtkWidget* toggle) { 213 jxl_save_opts.use_container = 214 !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); 215 return true; 216 } 217 218 bool JpegXlSaveGui::GuiOnChangeNoXYB(GtkWidget* toggle) { 219 jxl_save_opts.basic_info.uses_original_profile = 220 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); 221 return true; 222 } 223 224 bool JpegXlSaveGui::GuiOnChangeAdvancedMode(GtkWidget* toggle, 225 void* this_pointer) { 226 JpegXlSaveGui* self = static_cast<JpegXlSaveGui*>(this_pointer); 227 jxl_save_opts.advanced_mode = 228 gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)); 229 230 gtk_widget_set_sensitive(self->frame_advanced, jxl_save_opts.advanced_mode); 231 232 if (!jxl_save_opts.advanced_mode) { 233 jxl_save_opts.basic_info.uses_original_profile = JXL_FALSE; 234 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_no_xyb), false); 235 236 jxl_save_opts.use_container = true; 237 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->toggle_raw), false); 238 239 jxl_save_opts.faster_decoding = 0; 240 gtk_adjustment_set_value(GTK_ADJUSTMENT(self->entry_faster), 0); 241 } 242 return true; 243 } 244 245 bool JpegXlSaveGui::SaveDialog() { 246 gboolean run; 247 GtkWidget* dialog; 248 GtkWidget* content_area; 249 GtkWidget* main_vbox; 250 GtkWidget* frame; 251 GtkWidget* toggle; 252 GtkWidget* table; 253 GtkWidget* vbox; 254 GtkWidget* separator; 255 256 // initialize export dialog 257 gimp_ui_init(PLUG_IN_BINARY, true); 258 dialog = gimp_export_dialog_new("JPEG XL", PLUG_IN_BINARY, SAVE_PROC); 259 260 gtk_window_set_resizable(GTK_WINDOW(dialog), false); 261 content_area = gimp_export_dialog_get_content_area(dialog); 262 263 main_vbox = gtk_vbox_new(false, 6); 264 gtk_container_set_border_width(GTK_CONTAINER(main_vbox), 6); 265 gtk_box_pack_start(GTK_BOX(content_area), main_vbox, true, true, 0); 266 gtk_widget_show(main_vbox); 267 268 // Standard Settings Frame 269 frame = gtk_frame_new(nullptr); 270 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_ETCHED_IN); 271 gtk_box_pack_start(GTK_BOX(main_vbox), frame, false, false, 0); 272 gtk_widget_show(frame); 273 274 vbox = gtk_vbox_new(false, 6); 275 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6); 276 gtk_container_add(GTK_CONTAINER(frame), vbox); 277 gtk_widget_show(vbox); 278 279 // Layout Table 280 table = gtk_table_new(20, 3, false); 281 gtk_table_set_col_spacings(GTK_TABLE(table), 6); 282 gtk_box_pack_start(GTK_BOX(vbox), table, false, false, 0); 283 gtk_widget_show(table); 284 285 // Distance Slider 286 static gchar distance_help[] = 287 "Butteraugli distance target. Suggested values:" 288 "\n\td\u00A0=\u00A00.3\tExcellent" 289 "\n\td\u00A0=\u00A01\tVery Good" 290 "\n\td\u00A0=\u00A02\tGood" 291 "\n\td\u00A0=\u00A03\tFair" 292 "\n\td\u00A0=\u00A06\tPoor"; 293 294 entry_distance = reinterpret_cast<GtkAdjustment*>( 295 gimp_scale_entry_new(GTK_TABLE(table), 0, 0, "Distance", SCALE_WIDTH, 0, 296 jxl_save_opts.distance, 0.0, 15.0, 0.001, 1.0, 3, 297 true, 0.0, 0.0, distance_help, SAVE_PROC)); 298 gimp_scale_entry_set_logarithmic(reinterpret_cast<GtkObject*>(entry_distance), 299 true); 300 301 // Quality Slider 302 static gchar quality_help[] = 303 "JPEG-style Quality is remapped to distance. " 304 "Values roughly match libjpeg quality settings."; 305 entry_quality = reinterpret_cast<GtkAdjustment*>(gimp_scale_entry_new( 306 GTK_TABLE(table), 0, 1, "Quality", SCALE_WIDTH, 0, jxl_save_opts.quality, 307 8.26, 100.0, 1.0, 10.0, 2, true, 0.0, 0.0, quality_help, SAVE_PROC)); 308 309 // Distance and Quality Signals 310 handle_entry_distance = g_signal_connect( 311 entry_distance, "value-changed", G_CALLBACK(GuiOnChangeDistance), this); 312 handle_entry_quality = g_signal_connect(entry_quality, "value-changed", 313 G_CALLBACK(GuiOnChangeQuality), this); 314 315 // ---------- 316 separator = gtk_vseparator_new(); 317 gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 2, 3, GTK_EXPAND, 318 GTK_EXPAND, 9, 9); 319 gtk_widget_show(separator); 320 321 // Encoding Effort / Speed 322 static gchar effort_help[] = 323 "Adjust encoding speed. Higher values are faster because " 324 "the encoder uses less effort to hit distance targets. " 325 "As\u00A0a\u00A0result, image quality may be decreased. " 326 "Default\u00A0=\u00A03."; 327 entry_effort = reinterpret_cast<GtkAdjustment*>( 328 gimp_scale_entry_new(GTK_TABLE(table), 0, 3, "Speed", SCALE_WIDTH, 0, 329 10 - jxl_save_opts.encoding_effort, 1, 9, 1, 2, 0, 330 true, 0.0, 0.0, effort_help, SAVE_PROC)); 331 332 // effort signal 333 g_signal_connect(entry_effort, "value-changed", G_CALLBACK(GuiOnChangeEffort), 334 nullptr); 335 336 // ---------- 337 separator = gtk_vseparator_new(); 338 gtk_table_attach(GTK_TABLE(table), separator, 0, 2, 4, 5, GTK_EXPAND, 339 GTK_EXPAND, 9, 9); 340 gtk_widget_show(separator); 341 342 // Lossless Mode Convenience Checkbox 343 static gchar lossless_help[] = 344 "Compress using modular lossless mode. " 345 "Speed\u00A0is adjusted to improve performance."; 346 toggle_lossless = gtk_check_button_new_with_label("Lossless Mode"); 347 gimp_help_set_help_data(toggle_lossless, lossless_help, nullptr); 348 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_lossless), 349 jxl_save_opts.lossless); 350 gtk_table_attach_defaults(GTK_TABLE(table), toggle_lossless, 0, 2, 5, 6); 351 gtk_widget_show(toggle_lossless); 352 353 // lossless signal 354 handle_toggle_lossless = g_signal_connect( 355 toggle_lossless, "toggled", G_CALLBACK(GuiOnChangeLossless), this); 356 357 // ---------- 358 separator = gtk_vseparator_new(); 359 gtk_box_pack_start(GTK_BOX(main_vbox), separator, false, false, 1); 360 gtk_widget_show(separator); 361 362 // Advanced Settings Frame 363 frame_advanced = gtk_frame_new("Advanced Settings"); 364 gimp_help_set_help_data(frame_advanced, 365 "Some advanced settings may produce malformed files.", 366 nullptr); 367 gtk_frame_set_shadow_type(GTK_FRAME(frame_advanced), GTK_SHADOW_ETCHED_IN); 368 gtk_box_pack_start(GTK_BOX(main_vbox), frame_advanced, true, true, 0); 369 gtk_widget_show(frame_advanced); 370 371 gtk_widget_set_sensitive(frame_advanced, false); 372 373 vbox = gtk_vbox_new(false, 6); 374 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6); 375 gtk_container_add(GTK_CONTAINER(frame_advanced), vbox); 376 gtk_widget_show(vbox); 377 378 // uses_original_profile 379 static gchar uses_original_profile_help[] = 380 "Prevents conversion to the XYB colorspace. " 381 "File sizes are approximately doubled."; 382 toggle_no_xyb = gtk_check_button_new_with_label("Do not use XYB colorspace"); 383 gimp_help_set_help_data(toggle_no_xyb, uses_original_profile_help, nullptr); 384 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_no_xyb), 385 jxl_save_opts.basic_info.uses_original_profile); 386 gtk_box_pack_start(GTK_BOX(vbox), toggle_no_xyb, false, false, 0); 387 gtk_widget_show(toggle_no_xyb); 388 389 g_signal_connect(toggle_no_xyb, "toggled", G_CALLBACK(GuiOnChangeNoXYB), 390 nullptr); 391 392 // save raw codestream 393 static gchar codestream_help[] = 394 "Save the raw codestream, without a container. " 395 "The container is required for metadata and some other features."; 396 toggle_raw = gtk_check_button_new_with_label("Save Raw Codestream"); 397 gimp_help_set_help_data(toggle_raw, codestream_help, nullptr); 398 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle_raw), 399 !jxl_save_opts.use_container); 400 gtk_box_pack_start(GTK_BOX(vbox), toggle_raw, false, false, 0); 401 gtk_widget_show(toggle_raw); 402 403 g_signal_connect(toggle_raw, "toggled", G_CALLBACK(GuiOnChangeCodestream), 404 nullptr); 405 406 // ---------- 407 separator = gtk_vseparator_new(); 408 gtk_box_pack_start(GTK_BOX(vbox), separator, false, false, 1); 409 gtk_widget_show(separator); 410 411 // Faster Decoding / Decoding Speed 412 static gchar faster_help[] = 413 "Improve decoding speed at the expense of quality. " 414 "Default\u00A0=\u00A00."; 415 table = gtk_table_new(1, 3, false); 416 gtk_table_set_col_spacings(GTK_TABLE(table), 6); 417 gtk_container_add(GTK_CONTAINER(vbox), table); 418 gtk_widget_show(table); 419 420 entry_faster = reinterpret_cast<GtkAdjustment*>( 421 gimp_scale_entry_new(GTK_TABLE(table), 0, 0, "Faster Decoding", 422 SCALE_WIDTH, 0, jxl_save_opts.faster_decoding, 0, 4, 423 1, 1, 0, true, 0.0, 0.0, faster_help, SAVE_PROC)); 424 425 // Faster Decoding Signals 426 g_signal_connect(entry_faster, "value-changed", 427 G_CALLBACK(gimp_int_adjustment_update), 428 &jxl_save_opts.faster_decoding); 429 430 // Enable Advanced Settings 431 frame = gtk_frame_new(nullptr); 432 gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_NONE); 433 gtk_box_pack_start(GTK_BOX(main_vbox), frame, true, true, 0); 434 gtk_widget_show(frame); 435 436 vbox = gtk_vbox_new(false, 6); 437 gtk_container_set_border_width(GTK_CONTAINER(vbox), 6); 438 gtk_container_add(GTK_CONTAINER(frame), vbox); 439 gtk_widget_show(vbox); 440 441 static gchar advanced_help[] = 442 "Some advanced settings may produce malformed files."; 443 toggle = gtk_check_button_new_with_label("Enable Advanced Settings"); 444 gimp_help_set_help_data(toggle, advanced_help, nullptr); 445 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), 446 jxl_save_opts.advanced_mode); 447 gtk_box_pack_start(GTK_BOX(vbox), toggle, false, false, 0); 448 gtk_widget_show(toggle); 449 450 g_signal_connect(toggle, "toggled", G_CALLBACK(GuiOnChangeAdvancedMode), 451 this); 452 453 // show dialog 454 gtk_widget_show(dialog); 455 456 GtkAllocation allocation; 457 gtk_widget_get_allocation(dialog, &allocation); 458 459 int height = allocation.height; 460 gtk_widget_set_size_request(dialog, height * 1.5, height); 461 462 run = (gimp_dialog_run(GIMP_DIALOG(dialog)) == GTK_RESPONSE_OK); 463 gtk_widget_destroy(dialog); 464 465 return run; 466 } // JpegXlSaveGui::SaveDialog 467 468 JpegXlSaveOpts::JpegXlSaveOpts() { 469 SetDistance(1.0); 470 471 pixel_format.num_channels = 4; 472 pixel_format.data_type = JXL_TYPE_FLOAT; 473 pixel_format.endianness = JXL_NATIVE_ENDIAN; 474 pixel_format.align = 0; 475 476 JxlEncoderInitBasicInfo(&basic_info); 477 } // JpegXlSaveOpts constructor 478 479 bool JpegXlSaveOpts::SetModel(bool is_linear_) { 480 int channels; 481 std::string model; 482 483 if (is_gray) { 484 channels = 1; 485 if (is_linear_) { 486 model = "Y"; 487 } else { 488 model = "Y'"; 489 } 490 } else { 491 channels = 3; 492 if (is_linear_) { 493 model = "RGB"; 494 } else { 495 model = "R'G'B'"; 496 } 497 } 498 if (has_alpha) { 499 SetBablModel(model + "A"); 500 SetNumChannels(channels + 1); 501 } else { 502 SetBablModel(model); 503 SetNumChannels(channels); 504 } 505 return true; 506 } // JpegXlSaveOpts::SetModel 507 508 bool JpegXlSaveOpts::SetDistance(float dist) { 509 distance = dist; 510 return UpdateQuality(); 511 } 512 513 bool JpegXlSaveOpts::SetQuality(float qual) { 514 quality = qual; 515 return UpdateDistance(); 516 } 517 518 bool JpegXlSaveOpts::UpdateQuality() { 519 float qual; 520 521 if (distance < 0.1) { 522 qual = 100; 523 } else if (distance > 6.4) { 524 qual = -5.0 / 53.0 * sqrt(6360.0 * distance - 39975.0) + 1725.0 / 53.0; 525 lossless = false; 526 } else { 527 qual = 100 - (distance - 0.1) / 0.09; 528 lossless = false; 529 } 530 531 if (qual < 0) { 532 quality = 0.0; 533 } else if (qual >= 100) { 534 quality = 100.0; 535 } else { 536 quality = qual; 537 } 538 539 return true; 540 } 541 542 bool JpegXlSaveOpts::UpdateDistance() { 543 float dist = JxlEncoderDistanceFromQuality(quality); 544 545 if (dist > 25) { 546 distance = 25; 547 } else { 548 distance = dist; 549 } 550 return true; 551 } 552 553 bool JpegXlSaveOpts::SetDimensions(int x, int y) { 554 basic_info.xsize = x; 555 basic_info.ysize = y; 556 return true; 557 } 558 559 bool JpegXlSaveOpts::SetNumChannels(int channels) { 560 switch (channels) { 561 case 1: 562 pixel_format.num_channels = 1; 563 basic_info.num_color_channels = 1; 564 basic_info.num_extra_channels = 0; 565 basic_info.alpha_bits = 0; 566 basic_info.alpha_exponent_bits = 0; 567 break; 568 case 2: 569 pixel_format.num_channels = 2; 570 basic_info.num_color_channels = 1; 571 basic_info.num_extra_channels = 1; 572 basic_info.alpha_bits = 573 static_cast<int>(std::fmin(16, basic_info.bits_per_sample)); 574 basic_info.alpha_exponent_bits = 0; 575 break; 576 case 3: 577 pixel_format.num_channels = 3; 578 basic_info.num_color_channels = 3; 579 basic_info.num_extra_channels = 0; 580 basic_info.alpha_bits = 0; 581 basic_info.alpha_exponent_bits = 0; 582 break; 583 case 4: 584 pixel_format.num_channels = 4; 585 basic_info.num_color_channels = 3; 586 basic_info.num_extra_channels = 1; 587 basic_info.alpha_bits = 588 static_cast<int>(std::fmin(16, basic_info.bits_per_sample)); 589 basic_info.alpha_exponent_bits = 0; 590 break; 591 default: 592 SetNumChannels(3); 593 } // switch 594 return true; 595 } // JpegXlSaveOpts::SetNumChannels 596 597 bool JpegXlSaveOpts::UpdateBablFormat() { 598 babl_format_str = babl_model_str + " " + babl_type_str; 599 return true; 600 } 601 602 bool JpegXlSaveOpts::SetBablModel(std::string model) { 603 babl_model_str = std::move(model); 604 return UpdateBablFormat(); 605 } 606 607 bool JpegXlSaveOpts::SetBablType(std::string type) { 608 babl_type_str = std::move(type); 609 return UpdateBablFormat(); 610 } 611 612 bool JpegXlSaveOpts::SetPrecision(int gimp_precision) { 613 switch (gimp_precision) { 614 case GIMP_PRECISION_HALF_GAMMA: 615 case GIMP_PRECISION_HALF_LINEAR: 616 basic_info.bits_per_sample = 16; 617 basic_info.exponent_bits_per_sample = 5; 618 break; 619 620 // UINT32 not supported by encoder; using FLOAT instead 621 case GIMP_PRECISION_U32_GAMMA: 622 case GIMP_PRECISION_U32_LINEAR: 623 case GIMP_PRECISION_FLOAT_GAMMA: 624 case GIMP_PRECISION_FLOAT_LINEAR: 625 basic_info.bits_per_sample = 32; 626 basic_info.exponent_bits_per_sample = 8; 627 break; 628 629 case GIMP_PRECISION_U16_GAMMA: 630 case GIMP_PRECISION_U16_LINEAR: 631 basic_info.bits_per_sample = 16; 632 basic_info.exponent_bits_per_sample = 0; 633 break; 634 635 default: 636 case GIMP_PRECISION_U8_LINEAR: 637 case GIMP_PRECISION_U8_GAMMA: 638 basic_info.bits_per_sample = 8; 639 basic_info.exponent_bits_per_sample = 0; 640 break; 641 } 642 return true; 643 } // JpegXlSaveOpts::SetPrecision 644 645 } // namespace 646 647 bool SaveJpegXlImage(const gint32 image_id, const gint32 drawable_id, 648 const gint32 orig_image_id, const gchar* const filename) { 649 if (!jxl_save_gui.SaveDialog()) { 650 return true; 651 } 652 653 gint32 nlayers; 654 gint32* layers; 655 gint32 duplicate = gimp_image_duplicate(image_id); 656 657 JpegXlGimpProgress gimp_save_progress( 658 ("Saving JPEG XL file:" + std::string(filename)).c_str()); 659 gimp_save_progress.update(); 660 661 // try to get ICC color profile... 662 std::vector<uint8_t> icc; 663 664 GimpColorProfile* profile = gimp_image_get_effective_color_profile(image_id); 665 jxl_save_opts.is_gray = gimp_color_profile_is_gray(profile); 666 jxl_save_opts.is_linear = gimp_color_profile_is_linear(profile); 667 668 profile = gimp_image_get_color_profile(image_id); 669 if (profile) { 670 g_printerr(SAVE_PROC " Info: Extracting ICC Profile...\n"); 671 gsize icc_size; 672 const guint8* const icc_bytes = 673 gimp_color_profile_get_icc_profile(profile, &icc_size); 674 675 icc.assign(icc_bytes, icc_bytes + icc_size); 676 } else { 677 g_printerr(SAVE_PROC " Info: No ICC profile. Exporting image anyway.\n"); 678 } 679 680 gimp_save_progress.update(); 681 682 jxl_save_opts.SetDimensions(gimp_image_width(image_id), 683 gimp_image_height(image_id)); 684 685 jxl_save_opts.SetPrecision(gimp_image_get_precision(image_id)); 686 layers = gimp_image_get_layers(duplicate, &nlayers); 687 688 for (int i = 0; i < nlayers; i++) { 689 if (gimp_drawable_has_alpha(layers[i])) { 690 jxl_save_opts.has_alpha = true; 691 break; 692 } 693 } 694 695 gimp_save_progress.update(); 696 697 // layers need to match image size, for now 698 for (int i = 0; i < nlayers; i++) { 699 gimp_layer_resize_to_image_size(layers[i]); 700 } 701 702 // treat layers as animation frames, for now 703 if (nlayers > 1) { 704 jxl_save_opts.basic_info.have_animation = JXL_TRUE; 705 jxl_save_opts.basic_info.animation.tps_numerator = 100; 706 } 707 708 gimp_save_progress.update(); 709 710 // multi-threaded parallel runner. 711 auto runner = JxlResizableParallelRunnerMake(nullptr); 712 713 JxlResizableParallelRunnerSetThreads( 714 runner.get(), 715 JxlResizableParallelRunnerSuggestThreads(jxl_save_opts.basic_info.xsize, 716 jxl_save_opts.basic_info.ysize)); 717 718 auto enc = JxlEncoderMake(/*memory_manager=*/nullptr); 719 JxlEncoderUseContainer(enc.get(), jxl_save_opts.use_container); 720 721 if (JXL_ENC_SUCCESS != JxlEncoderSetParallelRunner(enc.get(), 722 JxlResizableParallelRunner, 723 runner.get())) { 724 g_printerr(SAVE_PROC " Error: JxlEncoderSetParallelRunner failed\n"); 725 return false; 726 } 727 728 // this sets some basic_info properties 729 jxl_save_opts.SetModel(jxl_save_opts.is_linear); 730 731 if (JXL_ENC_SUCCESS != 732 JxlEncoderSetBasicInfo(enc.get(), &jxl_save_opts.basic_info)) { 733 g_printerr(SAVE_PROC " Error: JxlEncoderSetBasicInfo failed\n"); 734 return false; 735 } 736 737 // try to use ICC profile 738 if (!icc.empty() && !jxl_save_opts.is_gray) { 739 if (JXL_ENC_SUCCESS == 740 JxlEncoderSetICCProfile(enc.get(), icc.data(), icc.size())) { 741 jxl_save_opts.icc_attached = true; 742 } else { 743 g_printerr(SAVE_PROC " Warning: JxlEncoderSetICCProfile failed.\n"); 744 jxl_save_opts.basic_info.uses_original_profile = JXL_FALSE; 745 jxl_save_opts.lossless = false; 746 } 747 } else { 748 g_printerr(SAVE_PROC " Warning: Using internal profile.\n"); 749 jxl_save_opts.basic_info.uses_original_profile = JXL_FALSE; 750 jxl_save_opts.lossless = false; 751 } 752 753 // set up internal color profile 754 JxlColorEncoding color_encoding = {}; 755 756 if (jxl_save_opts.is_linear) { 757 JxlColorEncodingSetToLinearSRGB(&color_encoding, 758 TO_JXL_BOOL(jxl_save_opts.is_gray)); 759 } else { 760 JxlColorEncodingSetToSRGB(&color_encoding, 761 TO_JXL_BOOL(jxl_save_opts.is_gray)); 762 } 763 764 if (JXL_ENC_SUCCESS != 765 JxlEncoderSetColorEncoding(enc.get(), &color_encoding)) { 766 g_printerr(SAVE_PROC " Warning: JxlEncoderSetColorEncoding failed\n"); 767 } 768 769 // set encoder options 770 JxlEncoderFrameSettings* frame_settings; 771 frame_settings = JxlEncoderFrameSettingsCreate(enc.get(), nullptr); 772 773 JxlEncoderFrameSettingsSetOption(frame_settings, JXL_ENC_FRAME_SETTING_EFFORT, 774 jxl_save_opts.encoding_effort); 775 JxlEncoderFrameSettingsSetOption(frame_settings, 776 JXL_ENC_FRAME_SETTING_DECODING_SPEED, 777 jxl_save_opts.faster_decoding); 778 779 // lossless mode 780 if (jxl_save_opts.lossless || jxl_save_opts.distance < 0.01) { 781 if (jxl_save_opts.basic_info.exponent_bits_per_sample > 0) { 782 // lossless mode doesn't work well with floating point 783 jxl_save_opts.distance = 0.01; 784 jxl_save_opts.lossless = false; 785 JxlEncoderSetFrameLossless(frame_settings, JXL_FALSE); 786 JxlEncoderSetFrameDistance(frame_settings, 0.01); 787 } else { 788 JxlEncoderSetFrameDistance(frame_settings, 0); 789 JxlEncoderSetFrameLossless(frame_settings, JXL_TRUE); 790 } 791 } else { 792 jxl_save_opts.lossless = false; 793 JxlEncoderSetFrameLossless(frame_settings, JXL_FALSE); 794 JxlEncoderSetFrameDistance(frame_settings, jxl_save_opts.distance); 795 } 796 797 // convert precision and colorspace 798 if (jxl_save_opts.is_linear && 799 jxl_save_opts.basic_info.bits_per_sample < 32) { 800 gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_LINEAR); 801 } else { 802 gimp_image_convert_precision(duplicate, GIMP_PRECISION_FLOAT_GAMMA); 803 } 804 805 // process layers and compress into JXL 806 size_t buffer_size = 807 jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize * 808 jxl_save_opts.pixel_format.num_channels * 4; // bytes per sample 809 810 for (int i = nlayers - 1; i >= 0; i--) { 811 gimp_save_progress.update(); 812 813 // copy image into buffer... 814 gpointer pixels_buffer_1; 815 gpointer pixels_buffer_2; 816 pixels_buffer_1 = g_malloc(buffer_size); 817 pixels_buffer_2 = g_malloc(buffer_size); 818 819 gimp_layer_resize_to_image_size(layers[i]); 820 821 GeglBuffer* buffer = gimp_drawable_get_buffer(layers[i]); 822 823 // using gegl_buffer_set_format to get the format because 824 // gegl_buffer_get_format doesn't always get the original format 825 const Babl* native_format = gegl_buffer_set_format(buffer, nullptr); 826 827 gegl_buffer_get(buffer, 828 GEGL_RECTANGLE(0, 0, jxl_save_opts.basic_info.xsize, 829 jxl_save_opts.basic_info.ysize), 830 1.0, native_format, pixels_buffer_1, GEGL_AUTO_ROWSTRIDE, 831 GEGL_ABYSS_NONE); 832 g_clear_object(&buffer); 833 834 // use babl to fix gamma mismatch issues 835 jxl_save_opts.SetModel(jxl_save_opts.is_linear); 836 jxl_save_opts.pixel_format.data_type = JXL_TYPE_FLOAT; 837 jxl_save_opts.SetBablType("float"); 838 const Babl* destination_format = 839 babl_format(jxl_save_opts.babl_format_str.c_str()); 840 841 babl_process( 842 babl_fish(native_format, destination_format), pixels_buffer_1, 843 pixels_buffer_2, 844 jxl_save_opts.basic_info.xsize * jxl_save_opts.basic_info.ysize); 845 846 gimp_save_progress.update(); 847 848 // send layer to encoder 849 if (JXL_ENC_SUCCESS != 850 JxlEncoderAddImageFrame(frame_settings, &jxl_save_opts.pixel_format, 851 pixels_buffer_2, buffer_size)) { 852 g_printerr(SAVE_PROC " Error: JxlEncoderAddImageFrame failed\n"); 853 return false; 854 } 855 } 856 857 JxlEncoderCloseInput(enc.get()); 858 859 // get data from encoder 860 std::vector<uint8_t> compressed; 861 compressed.resize(262144); 862 uint8_t* next_out = compressed.data(); 863 size_t avail_out = compressed.size(); 864 865 JxlEncoderStatus process_result = JXL_ENC_NEED_MORE_OUTPUT; 866 while (process_result == JXL_ENC_NEED_MORE_OUTPUT) { 867 gimp_save_progress.update(); 868 869 process_result = JxlEncoderProcessOutput(enc.get(), &next_out, &avail_out); 870 if (process_result == JXL_ENC_NEED_MORE_OUTPUT) { 871 size_t offset = next_out - compressed.data(); 872 compressed.resize(compressed.size() + 262144); 873 next_out = compressed.data() + offset; 874 avail_out = compressed.size() - offset; 875 } 876 } 877 compressed.resize(next_out - compressed.data()); 878 879 if (JXL_ENC_SUCCESS != process_result) { 880 g_printerr(SAVE_PROC " Error: JxlEncoderProcessOutput failed\n"); 881 return false; 882 } 883 884 // write file 885 std::ofstream outstream(filename, std::ios::out | std::ios::binary); 886 copy(compressed.begin(), compressed.end(), 887 std::ostream_iterator<uint8_t>(outstream)); 888 889 gimp_save_progress.finished(); 890 return true; 891 } // SaveJpegXlImage() 892 893 } // namespace jxl