/* GIMP - The GNU Image Manipulation Program * Copyright (C) 1995 Spencer Kimball and Peter Mattis * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #include #include #include #include #include #include #ifdef HAVE_UNISTD_H #include #endif #include #include #include #ifdef HAVE_EXIF #include #endif /* HAVE_EXIF */ #include #include #include "libgimp/stdplugins-intl.h" #include "jpeg.h" #include "jpeg-icc.h" #include "jpeg-load.h" #include "jpeg-save.h" #include "jpeg-settings.h" #define SCALE_WIDTH 125 /* See bugs #63610 and #61088 for a discussion about the quality settings */ #define DEFAULT_QUALITY 85.0 #define DEFAULT_SMOOTHING 0.0 #define DEFAULT_OPTIMIZE TRUE #define DEFAULT_PROGRESSIVE FALSE #define DEFAULT_BASELINE TRUE #define DEFAULT_SUBSMP JPEG_SUPSAMPLING_2x2_1x1_1x1 #define DEFAULT_RESTART 0 #define DEFAULT_DCT 0 #define DEFAULT_PREVIEW FALSE #define DEFAULT_EXIF TRUE #define DEFAULT_THUMBNAIL FALSE #define DEFAULT_XMP TRUE #define DEFAULT_USE_ORIG_QUALITY FALSE #define JPEG_DEFAULTS_PARASITE "jpeg-save-defaults" typedef struct { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; gint tile_height; FILE *outfile; gboolean has_alpha; gint rowstride; guchar *temp; guchar *data; guchar *src; GimpDrawable *drawable; GimpPixelRgn pixel_rgn; const gchar *file_name; gboolean abort_me; guint source_id; } PreviewPersistent; /*le added : struct containing pointers to save dialog*/ typedef struct { gboolean run; GtkWidget *use_restart_markers; /*checkbox setting use restart markers*/ GtkTextBuffer *text_buffer; GtkObject *scale_data; /*for restart markers*/ gulong handler_id_restart; GtkObject *quality; /*quality slidebar*/ GtkObject *smoothing; /*smoothing slidebar*/ GtkWidget *optimize; /*optimize togle*/ GtkWidget *progressive; /*progressive toggle*/ GtkWidget *subsmp; /*subsampling side select*/ GtkWidget *restart; /*spinner for setting frequency restart markers*/ GtkWidget *dct; /*DCT side select*/ GtkWidget *preview; /*show preview toggle checkbox*/ GtkWidget *save_exif; GtkWidget *save_thumbnail; GtkWidget *save_xmp; GtkWidget *use_orig_quality; /*quant tables toggle*/ } JpegSaveGui; static void make_preview (void); static void save_restart_update (GtkAdjustment *adjustment, GtkWidget *toggle); static void subsampling_changed (GtkWidget *combo, GtkObject *entry); static void quality_changed (GtkObject *scale_entry, GtkWidget *toggle); static void use_orig_qual_changed (GtkWidget *toggle, GtkObject *scale_entry); static void use_orig_qual_changed2 (GtkWidget *toggle, GtkWidget *combo); #ifdef HAVE_EXIF static gint create_thumbnail (gint32 image_ID, gint32 drawable_ID, gdouble quality, guchar **thumbnail_buffer); #endif /* HAVE_EXIF */ static GtkWidget *restart_markers_scale = NULL; static GtkWidget *restart_markers_label = NULL; static GtkWidget *preview_size = NULL; static PreviewPersistent *prev_p = NULL; static void save_dialog_response (GtkWidget *widget, gint response_id, gpointer data); static void load_gui_defaults (JpegSaveGui *pg); static void save_defaults (void); /* * sg - This is the best I can do, I'm afraid... I think it will fail * if something bad really happens (but it might not). If you have a * better solution, send it ;-) */ static void background_error_exit (j_common_ptr cinfo) { if (prev_p) prev_p->abort_me = TRUE; (*cinfo->err->output_message) (cinfo); } static gboolean background_jpeg_save (PreviewPersistent *pp) { guchar *t; guchar *s; gint i, j; gint yend; if (pp->abort_me || (pp->cinfo.next_scanline >= pp->cinfo.image_height)) { /* clean up... */ if (pp->abort_me) { jpeg_abort_compress (&(pp->cinfo)); } else { jpeg_finish_compress (&(pp->cinfo)); } fclose (pp->outfile); jpeg_destroy_compress (&(pp->cinfo)); g_free (pp->temp); g_free (pp->data); if (pp->drawable) gimp_drawable_detach (pp->drawable); /* display the preview stuff */ if (!pp->abort_me) { struct stat buf; gchar *text; gchar *size_text; g_stat (pp->file_name, &buf); size_text = g_format_size_for_display (buf.st_size); text = g_strdup_printf (_("File size: %s"), size_text); gtk_label_set_text (GTK_LABEL (preview_size), text); g_free (text); g_free (size_text); /* and load the preview */ load_image (pp->file_name, GIMP_RUN_NONINTERACTIVE, TRUE, NULL); } /* we cleanup here (load_image doesn't run in the background) */ g_unlink (pp->file_name); g_free (pp); prev_p = NULL; gimp_displays_flush (); gdk_flush (); return FALSE; } else { if ((pp->cinfo.next_scanline % pp->tile_height) == 0) { yend = pp->cinfo.next_scanline + pp->tile_height; yend = MIN (yend, pp->cinfo.image_height); gimp_pixel_rgn_get_rect (&pp->pixel_rgn, pp->data, 0, pp->cinfo.next_scanline, pp->cinfo.image_width, (yend - pp->cinfo.next_scanline)); pp->src = pp->data; } t = pp->temp; s = pp->src; i = pp->cinfo.image_width; while (i--) { for (j = 0; j < pp->cinfo.input_components; j++) *t++ = *s++; if (pp->has_alpha) /* ignore alpha channel */ s++; } pp->src += pp->rowstride; jpeg_write_scanlines (&(pp->cinfo), (JSAMPARRAY) &(pp->temp), 1); return TRUE; } } gboolean save_image (const gchar *filename, gint32 image_ID, gint32 drawable_ID, gint32 orig_image_ID, gboolean preview, GError **error) { GimpPixelRgn pixel_rgn; GimpDrawable *drawable; GimpImageType drawable_type; GimpParasite *parasite; static struct jpeg_compress_struct cinfo; static struct my_error_mgr jerr; JpegSubsampling subsampling; FILE * volatile outfile; guchar *temp, *t; guchar *data; guchar *src, *s; gboolean has_alpha; gint rowstride, yend; gint i, j; drawable = gimp_drawable_get (drawable_ID); drawable_type = gimp_drawable_type (drawable_ID); gimp_pixel_rgn_init (&pixel_rgn, drawable, 0, 0, drawable->width, drawable->height, FALSE, FALSE); if (! preview) gimp_progress_init_printf (_("Saving '%s'"), gimp_filename_to_utf8 (filename)); /* Step 1: allocate and initialize JPEG compression object */ /* We have to set up the error handler first, in case the initialization * step fails. (Unlikely, but it could happen if you are out of memory.) * This routine fills in the contents of struct jerr, and returns jerr's * address which we place into the link field in cinfo. */ cinfo.err = jpeg_std_error (&jerr.pub); jerr.pub.error_exit = my_error_exit; outfile = NULL; /* Establish the setjmp return context for my_error_exit to use. */ if (setjmp (jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. * We need to clean up the JPEG object, close the input file, and return. */ jpeg_destroy_compress (&cinfo); if (outfile) fclose (outfile); if (drawable) gimp_drawable_detach (drawable); return FALSE; } /* Now we can initialize the JPEG compression object. */ jpeg_create_compress (&cinfo); /* Step 2: specify data destination (eg, a file) */ /* Note: steps 2 and 3 can be done in either order. */ /* Here we use the library-supplied code to send compressed data to a * stdio stream. You can also write your own code to do something else. * VERY IMPORTANT: use "b" option to fopen() if you are on a machine that * requires it in order to write binary files. */ if ((outfile = g_fopen (filename, "wb")) == NULL) { g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (errno), _("Could not open '%s' for writing: %s"), gimp_filename_to_utf8 (filename), g_strerror (errno)); return FALSE; } jpeg_stdio_dest (&cinfo, outfile); /* Get the input image and a pointer to its data. */ switch (drawable_type) { case GIMP_RGB_IMAGE: case GIMP_GRAY_IMAGE: /* # of color components per pixel */ cinfo.input_components = drawable->bpp; has_alpha = FALSE; break; case GIMP_RGBA_IMAGE: case GIMP_GRAYA_IMAGE: /* # of color components per pixel (minus the GIMP alpha channel) */ cinfo.input_components = drawable->bpp - 1; has_alpha = TRUE; break; case GIMP_INDEXED_IMAGE: return FALSE; default: return FALSE; } /* Step 3: set parameters for compression */ /* First we supply a description of the input image. * Four fields of the cinfo struct must be filled in: */ /* image width and height, in pixels */ cinfo.image_width = drawable->width; cinfo.image_height = drawable->height; /* colorspace of input image */ cinfo.in_color_space = (drawable_type == GIMP_RGB_IMAGE || drawable_type == GIMP_RGBA_IMAGE) ? JCS_RGB : JCS_GRAYSCALE; /* Now use the library's routine to set default compression parameters. * (You must set at least cinfo.in_color_space before calling this, * since the defaults depend on the source color space.) */ jpeg_set_defaults (&cinfo); jpeg_set_quality (&cinfo, (gint) (jsvals.quality + 0.5), jsvals.baseline); if (jsvals.use_orig_quality && num_quant_tables > 0) { guint **quant_tables; gint t; /* override tables generated by jpeg_set_quality() with custom tables */ quant_tables = jpeg_restore_original_tables (image_ID, num_quant_tables); if (quant_tables) { for (t = 0; t < num_quant_tables; t++) { jpeg_add_quant_table (&cinfo, t, quant_tables[t], 100, jsvals.baseline); g_free (quant_tables[t]); } g_free (quant_tables); } } cinfo.optimize_coding = jsvals.optimize; subsampling = (gimp_drawable_is_rgb (drawable_ID) ? jsvals.subsmp : JPEG_SUPSAMPLING_1x1_1x1_1x1); /* smoothing is not supported with nonstandard sampling ratios */ if (subsampling != JPEG_SUPSAMPLING_2x1_1x1_1x1 && subsampling != JPEG_SUPSAMPLING_1x2_1x1_1x1) { cinfo.smoothing_factor = (gint) (jsvals.smoothing * 100); } if (jsvals.progressive) { jpeg_simple_progression (&cinfo); } switch (subsampling) { case JPEG_SUPSAMPLING_2x2_1x1_1x1: default: cinfo.comp_info[0].h_samp_factor = 2; cinfo.comp_info[0].v_samp_factor = 2; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; break; case JPEG_SUPSAMPLING_2x1_1x1_1x1: cinfo.comp_info[0].h_samp_factor = 2; cinfo.comp_info[0].v_samp_factor = 1; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; break; case JPEG_SUPSAMPLING_1x1_1x1_1x1: cinfo.comp_info[0].h_samp_factor = 1; cinfo.comp_info[0].v_samp_factor = 1; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; break; case JPEG_SUPSAMPLING_1x2_1x1_1x1: cinfo.comp_info[0].h_samp_factor = 1; cinfo.comp_info[0].v_samp_factor = 2; cinfo.comp_info[1].h_samp_factor = 1; cinfo.comp_info[1].v_samp_factor = 1; cinfo.comp_info[2].h_samp_factor = 1; cinfo.comp_info[2].v_samp_factor = 1; break; } cinfo.restart_interval = 0; cinfo.restart_in_rows = jsvals.restart; switch (jsvals.dct) { case 0: default: cinfo.dct_method = JDCT_ISLOW; break; case 1: cinfo.dct_method = JDCT_IFAST; break; case 2: cinfo.dct_method = JDCT_FLOAT; break; } { gdouble xresolution; gdouble yresolution; gimp_image_get_resolution (orig_image_ID, &xresolution, &yresolution); if (xresolution > 1e-5 && yresolution > 1e-5) { gdouble factor; factor = gimp_unit_get_factor (gimp_image_get_unit (orig_image_ID)); if (factor == 2.54 /* cm */ || factor == 25.4 /* mm */) { cinfo.density_unit = 2; /* dots per cm */ xresolution /= 2.54; yresolution /= 2.54; } else { cinfo.density_unit = 1; /* dots per inch */ } cinfo.X_density = xresolution; cinfo.Y_density = yresolution; } } /* Step 4: Start compressor */ /* TRUE ensures that we will write a complete interchange-JPEG file. * Pass TRUE unless you are very sure of what you're doing. */ jpeg_start_compress (&cinfo, TRUE); #ifdef HAVE_EXIF /* Create the thumbnail JPEG in a buffer */ if ((jsvals.save_exif && exif_data) || jsvals.save_thumbnail) { ExifData *exif_data_tmp = NULL; guchar *exif_buf = NULL; guchar *thumbnail_buffer = NULL; gint thumbnail_buffer_length = 0; guint exif_buf_len; gdouble quality = MIN (75.0, jsvals.quality); if ( (! jsvals.save_exif) || (! exif_data)) exif_data_tmp = exif_data_new (); else exif_data_tmp = exif_data; /* avoid saving markers longer than 65533, gradually decrease * quality in steps of 5 until exif_buf_len is lower than that. */ for (exif_buf_len = 65535; exif_buf_len > 65533 && quality > 0.0; quality -= 5.0) { if (jsvals.save_thumbnail) thumbnail_buffer_length = create_thumbnail (image_ID, drawable_ID, quality, &thumbnail_buffer); exif_data_tmp->data = thumbnail_buffer; exif_data_tmp->size = thumbnail_buffer_length; if (exif_buf) free (exif_buf); exif_data_save_data (exif_data_tmp, &exif_buf, &exif_buf_len); } if (exif_buf_len > 65533) { /* last attempt with quality 0.0 */ if (jsvals.save_thumbnail) thumbnail_buffer_length = create_thumbnail (image_ID, drawable_ID, 0.0, &thumbnail_buffer); exif_data_tmp->data = thumbnail_buffer; exif_data_tmp->size = thumbnail_buffer_length; if (exif_buf) free (exif_buf); exif_data_save_data (exif_data_tmp, &exif_buf, &exif_buf_len); } if (exif_buf_len > 65533) { /* still no go? save without thumbnail */ exif_data_tmp->data = NULL; exif_data_tmp->size = 0; if (exif_buf) free (exif_buf); exif_data_save_data (exif_data_tmp, &exif_buf, &exif_buf_len); } #ifdef GIMP_UNSTABLE g_print ("jpeg-save: saving EXIF block (%d bytes)\n", exif_buf_len); #endif jpeg_write_marker (&cinfo, JPEG_APP0 + 1, exif_buf, exif_buf_len); if (exif_buf) free (exif_buf); } #endif /* HAVE_EXIF */ /* Step 4.1: Write the comment out - pw */ if (image_comment && *image_comment) { #ifdef GIMP_UNSTABLE g_print ("jpeg-save: saving image comment (%d bytes)\n", (int) strlen (image_comment)); #endif jpeg_write_marker (&cinfo, JPEG_COM, (guchar *) image_comment, strlen (image_comment)); } /* Step 4.2: Write the XMP packet in an APP1 marker */ if (jsvals.save_xmp) { /* FIXME: temporary hack until the right thing is done by a library */ parasite = gimp_image_get_parasite (orig_image_ID, "gimp-metadata"); if (parasite) { const gchar *xmp_data; glong xmp_data_size; guchar *app_block; xmp_data = ((const gchar *) gimp_parasite_data (parasite)) + 10; xmp_data_size = gimp_parasite_data_size (parasite) - 10; #ifdef GIMP_UNSTABLE g_print ("jpeg-save: saving XMP packet (%d bytes)\n", (int) xmp_data_size); #endif app_block = g_malloc (sizeof (JPEG_APP_HEADER_XMP) + xmp_data_size); memcpy (app_block, JPEG_APP_HEADER_XMP, sizeof (JPEG_APP_HEADER_XMP)); memcpy (app_block + sizeof (JPEG_APP_HEADER_XMP), xmp_data, xmp_data_size); jpeg_write_marker (&cinfo, JPEG_APP0 + 1, app_block, sizeof (JPEG_APP_HEADER_XMP) + xmp_data_size); g_free (app_block); gimp_parasite_free (parasite); } } /* Step 4.3: store the color profile if there is one */ parasite = gimp_image_get_parasite (orig_image_ID, "icc-profile"); if (parasite) { jpeg_icc_write_profile (&cinfo, gimp_parasite_data (parasite), gimp_parasite_data_size (parasite)); gimp_parasite_free (parasite); } /* Step 5: while (scan lines remain to be written) */ /* jpeg_write_scanlines(...); */ /* Here we use the library's state variable cinfo.next_scanline as the * loop counter, so that we don't have to keep track ourselves. * To keep things simple, we pass one scanline per call; you can pass * more if you wish, though. */ /* JSAMPLEs per row in image_buffer */ rowstride = drawable->bpp * drawable->width; temp = g_new (guchar, cinfo.image_width * cinfo.input_components); data = g_new (guchar, rowstride * gimp_tile_height ()); /* fault if cinfo.next_scanline isn't initially a multiple of * gimp_tile_height */ src = NULL; /* * sg - if we preview, we want this to happen in the background -- do * not duplicate code in the future; for now, it's OK */ if (preview) { PreviewPersistent *pp = g_new (PreviewPersistent, 1); /* pass all the information we need */ pp->cinfo = cinfo; pp->tile_height = gimp_tile_height(); pp->data = data; pp->outfile = outfile; pp->has_alpha = has_alpha; pp->rowstride = rowstride; pp->temp = temp; pp->data = data; pp->drawable = drawable; pp->pixel_rgn = pixel_rgn; pp->src = NULL; pp->file_name = filename; pp->abort_me = FALSE; g_warn_if_fail (prev_p == NULL); prev_p = pp; pp->cinfo.err = jpeg_std_error(&(pp->jerr)); pp->jerr.error_exit = background_error_exit; gtk_label_set_text (GTK_LABEL (preview_size), _("Calculating file size...")); pp->source_id = g_idle_add ((GSourceFunc) background_jpeg_save, pp); /* background_jpeg_save() will cleanup as needed */ return TRUE; } while (cinfo.next_scanline < cinfo.image_height) { if ((cinfo.next_scanline % gimp_tile_height ()) == 0) { yend = cinfo.next_scanline + gimp_tile_height (); yend = MIN (yend, cinfo.image_height); gimp_pixel_rgn_get_rect (&pixel_rgn, data, 0, cinfo.next_scanline, cinfo.image_width, (yend - cinfo.next_scanline)); src = data; } t = temp; s = src; i = cinfo.image_width; while (i--) { for (j = 0; j < cinfo.input_components; j++) *t++ = *s++; if (has_alpha) /* ignore alpha channel */ s++; } src += rowstride; jpeg_write_scanlines (&cinfo, (JSAMPARRAY) &temp, 1); if ((cinfo.next_scanline % 32) == 0) gimp_progress_update ((gdouble) cinfo.next_scanline / (gdouble) cinfo.image_height); } /* Step 6: Finish compression */ jpeg_finish_compress (&cinfo); /* After finish_compress, we can close the output file. */ fclose (outfile); /* Step 7: release JPEG compression object */ /* This is an important step since it will release a good deal of memory. */ jpeg_destroy_compress (&cinfo); /* free the temporary buffer */ g_free (temp); g_free (data); /* And we're done! */ gimp_progress_update (1.0); gimp_drawable_detach (drawable); return TRUE; } static void make_preview (void) { destroy_preview (); if (jsvals.preview) { gchar *tn = gimp_temp_name ("jpeg"); if (! undo_touched) { /* we freeze undo saving so that we can avoid sucking up * tile cache with our unneeded preview steps. */ gimp_image_undo_freeze (preview_image_ID); undo_touched = TRUE; } save_image (tn, preview_image_ID, drawable_ID_global, orig_image_ID_global, TRUE, NULL); if (display_ID == -1) display_ID = gimp_display_new (preview_image_ID); } else { gtk_label_set_text (GTK_LABEL (preview_size), _("File size: unknown")); gimp_displays_flush (); } } void destroy_preview (void) { if (prev_p && !prev_p->abort_me) { guint id = prev_p->source_id; prev_p->abort_me = TRUE; /* signal the background save to stop */ background_jpeg_save (prev_p); g_source_remove (id); } if (drawable_global) { gimp_drawable_detach (drawable_global); drawable_global = NULL; } if (gimp_image_is_valid (preview_image_ID) && gimp_item_is_valid (preview_layer_ID)) { /* assuming that reference counting is working correctly, we do not need to delete the layer, removing it from the image should be sufficient */ gimp_image_remove_layer (preview_image_ID, preview_layer_ID); preview_layer_ID = -1; } } gboolean save_dialog (void) { JpegSaveGui pg; GtkWidget *dialog; GtkWidget *vbox; GtkObject *entry; GtkWidget *table; GtkWidget *table2; GtkWidget *tabledefaults; GtkWidget *expander; GtkWidget *frame; GtkWidget *toggle; GtkWidget *spinbutton; GtkWidget *label; GtkWidget *combo; GtkWidget *text_view; GtkTextBuffer *text_buffer; GtkWidget *scrolled_window; GtkWidget *button; gchar *text; dialog = gimp_export_dialog_new (_("JPEG"), PLUG_IN_BINARY, SAVE_PROC); g_signal_connect (dialog, "response", G_CALLBACK (save_dialog_response), &pg); g_signal_connect (dialog, "destroy", G_CALLBACK (gtk_main_quit), NULL); gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); vbox = gtk_vbox_new (FALSE, 12); gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (dialog)), vbox, TRUE, TRUE, 0); gtk_widget_show (vbox); table = gtk_table_new (1, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table), 6); gtk_box_pack_start (GTK_BOX (vbox), table, FALSE, FALSE, 0); gtk_widget_show (table); pg.quality = entry = gimp_scale_entry_new (GTK_TABLE (table), 0, 0, _("_Quality:"), SCALE_WIDTH, 0, jsvals.quality, 0.0, 100.0, 1.0, 10.0, 0, TRUE, 0.0, 0.0, _("JPEG quality parameter"), "file-jpeg-save-quality"); g_signal_connect (entry, "value-changed", G_CALLBACK (gimp_double_adjustment_update), &jsvals.quality); g_signal_connect (entry, "value-changed", G_CALLBACK (make_preview), NULL); preview_size = gtk_label_new (_("File size: unknown")); gtk_misc_set_alignment (GTK_MISC (preview_size), 0.0, 0.5); gimp_label_set_attributes (GTK_LABEL (preview_size), PANGO_ATTR_STYLE, PANGO_STYLE_ITALIC, -1); gtk_box_pack_start (GTK_BOX (vbox), preview_size, FALSE, FALSE, 0); gtk_widget_show (preview_size); gimp_help_set_help_data (preview_size, _("Enable preview to obtain the file size."), NULL); pg.preview = toggle = gtk_check_button_new_with_mnemonic (_("Sho_w preview in image window")); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.preview); gtk_box_pack_start (GTK_BOX (vbox), toggle, FALSE, FALSE, 0); gtk_widget_show (toggle); g_signal_connect (toggle, "toggled", G_CALLBACK (gimp_toggle_button_update), &jsvals.preview); g_signal_connect (toggle, "toggled", G_CALLBACK (make_preview), NULL); text = g_strdup_printf ("%s", _("_Advanced Options")); expander = gtk_expander_new_with_mnemonic (text); gtk_expander_set_use_markup (GTK_EXPANDER (expander), TRUE); g_free (text); gtk_box_pack_start (GTK_BOX (vbox), expander, TRUE, TRUE, 0); gtk_widget_show (expander); vbox = gtk_vbox_new (FALSE, 12); gtk_container_add (GTK_CONTAINER (expander), vbox); gtk_widget_show (vbox); frame = gimp_frame_new (""); gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0); gtk_widget_show (frame); table = gtk_table_new (4, 7, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table), 6); gtk_table_set_row_spacings (GTK_TABLE (table), 6); gtk_table_set_col_spacing (GTK_TABLE (table), 1, 12); gtk_container_add (GTK_CONTAINER (frame), table); table2 = gtk_table_new (1, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (table2), 6); gtk_table_attach (GTK_TABLE (table), table2, 2, 6, 0, 1, GTK_FILL, GTK_FILL, 0, 0); gtk_widget_show (table2); pg.smoothing = entry = gimp_scale_entry_new (GTK_TABLE (table2), 0, 0, _("S_moothing:"), 100, 0, jsvals.smoothing, 0.0, 1.0, 0.01, 0.1, 2, TRUE, 0.0, 0.0, NULL, "file-jpeg-save-smoothing"); g_signal_connect (entry, "value-changed", G_CALLBACK (gimp_double_adjustment_update), &jsvals.smoothing); g_signal_connect (entry, "value-changed", G_CALLBACK (make_preview), NULL); restart_markers_label = gtk_label_new (_("Frequency (rows):")); gtk_misc_set_alignment (GTK_MISC (restart_markers_label), 1.0, 0.5); gtk_table_attach (GTK_TABLE (table), restart_markers_label, 4, 5, 1, 2, GTK_FILL | GTK_EXPAND, GTK_FILL, 0, 0); gtk_widget_show (restart_markers_label); /*pg.scale_data = scale_data;*/ pg.restart = restart_markers_scale = spinbutton = gimp_spin_button_new (&pg.scale_data, (jsvals.restart == 0) ? 1 : jsvals.restart, 1.0, 64.0, 1.0, 1.0, 0, 1.0, 0); gtk_table_attach (GTK_TABLE (table), spinbutton, 5, 6, 1, 2, GTK_FILL, GTK_FILL, 0, 0); gtk_widget_show (spinbutton); pg.use_restart_markers = toggle = gtk_check_button_new_with_mnemonic (_("Use _restart markers")); gtk_table_attach (GTK_TABLE (table), toggle, 2, 4, 1, 2, GTK_FILL, 0, 0, 0); gtk_widget_show (toggle); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.restart); gtk_widget_set_sensitive (restart_markers_label, jsvals.restart); gtk_widget_set_sensitive (restart_markers_scale, jsvals.restart); g_signal_connect (pg.scale_data, "value-changed", G_CALLBACK (save_restart_update), toggle); pg.handler_id_restart = g_signal_connect_swapped (toggle, "toggled", G_CALLBACK (save_restart_update), pg.scale_data); pg.optimize = toggle = gtk_check_button_new_with_mnemonic (_("_Optimize")); gtk_table_attach (GTK_TABLE (table), toggle, 0, 1, 0, 1, GTK_FILL, 0, 0, 0); gtk_widget_show (toggle); g_signal_connect (toggle, "toggled", G_CALLBACK (gimp_toggle_button_update), &jsvals.optimize); g_signal_connect (toggle, "toggled", G_CALLBACK (make_preview), NULL); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.optimize); pg.progressive = toggle = gtk_check_button_new_with_mnemonic (_("_Progressive")); gtk_table_attach (GTK_TABLE (table), toggle, 0, 1, 1, 2, GTK_FILL, 0, 0, 0); gtk_widget_show (toggle); g_signal_connect (toggle, "toggled", G_CALLBACK (gimp_toggle_button_update), &jsvals.progressive); g_signal_connect (toggle, "toggled", G_CALLBACK (make_preview), NULL); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.progressive); #ifdef HAVE_EXIF pg.save_exif = toggle = gtk_check_button_new_with_mnemonic (_("Save _EXIF data")); gtk_table_attach (GTK_TABLE (table), toggle, 0, 1, 2, 3, GTK_FILL, 0, 0, 0); gtk_widget_show (toggle); g_signal_connect (toggle, "toggled", G_CALLBACK (gimp_toggle_button_update), &jsvals.save_exif); g_signal_connect (toggle, "toggled", G_CALLBACK (make_preview), NULL); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.save_exif && exif_data); gtk_widget_set_sensitive (toggle, exif_data != NULL); pg.save_thumbnail = toggle = gtk_check_button_new_with_mnemonic (_("Save _thumbnail")); gtk_table_attach (GTK_TABLE (table), toggle, 0, 1, 3, 4, GTK_FILL, 0, 0, 0); gtk_widget_show (toggle); g_signal_connect (toggle, "toggled", G_CALLBACK (gimp_toggle_button_update), &jsvals.save_thumbnail); g_signal_connect (toggle, "toggled", G_CALLBACK (make_preview), NULL); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.save_thumbnail); #endif /* HAVE_EXIF */ /* XMP metadata */ pg.save_xmp = toggle = gtk_check_button_new_with_mnemonic (_("Save _XMP data")); gtk_table_attach (GTK_TABLE (table), toggle, 0, 1, 4, 5, GTK_FILL, 0, 0, 0); gtk_widget_show (toggle); g_signal_connect (toggle, "toggled", G_CALLBACK (gimp_toggle_button_update), &jsvals.save_xmp); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.save_xmp && has_metadata); gtk_widget_set_sensitive (toggle, has_metadata); /* custom quantization tables - now used also for original quality */ pg.use_orig_quality = toggle = gtk_check_button_new_with_mnemonic (_("_Use quality settings from original " "image")); gtk_table_attach (GTK_TABLE (table), toggle, 0, 4, 5, 6, GTK_FILL, 0, 0, 0); gtk_widget_show (toggle); gimp_help_set_help_data (toggle, _("If the original image was loaded from a JPEG " "file using non-standard quality settings " "(quantization tables), enable this option to " "get almost the same quality and file size."), NULL); g_signal_connect (toggle, "toggled", G_CALLBACK (gimp_toggle_button_update), &jsvals.use_orig_quality); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), jsvals.use_orig_quality && (orig_quality > 0)); gtk_widget_set_sensitive (toggle, (orig_quality > 0)); /* changing quality disables custom quantization tables, and vice-versa */ g_signal_connect (pg.quality, "value-changed", G_CALLBACK (quality_changed), pg.use_orig_quality); g_signal_connect (pg.use_orig_quality, "toggled", G_CALLBACK (use_orig_qual_changed), pg.quality); /* Subsampling */ label = gtk_label_new_with_mnemonic (_("Su_bsampling:")); gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); gtk_table_attach (GTK_TABLE (table), label, 2, 3, 2, 3, GTK_FILL, GTK_FILL, 0, 0); gtk_widget_show (label); pg.subsmp = combo = gimp_int_combo_box_new (_("1x1,1x1,1x1 (best quality)"), JPEG_SUPSAMPLING_1x1_1x1_1x1, _("2x1,1x1,1x1 (4:2:2)"), JPEG_SUPSAMPLING_2x1_1x1_1x1, _("1x2,1x1,1x1"), JPEG_SUPSAMPLING_1x2_1x1_1x1, _("2x2,1x1,1x1 (smallest file)"), JPEG_SUPSAMPLING_2x2_1x1_1x1, NULL); gtk_table_attach (GTK_TABLE (table), combo, 3, 6, 2, 3, GTK_FILL | GTK_EXPAND, GTK_FILL, 0, 0); gtk_widget_show (combo); gtk_label_set_mnemonic_widget (GTK_LABEL (label), combo); if (gimp_drawable_is_rgb (drawable_ID_global)) { gimp_int_combo_box_connect (GIMP_INT_COMBO_BOX (combo), jsvals.subsmp, G_CALLBACK (subsampling_changed), entry); g_signal_connect (pg.use_orig_quality, "toggled", G_CALLBACK (use_orig_qual_changed2), pg.subsmp); } else { gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (combo), JPEG_SUPSAMPLING_1x1_1x1_1x1); gtk_widget_set_sensitive (combo, FALSE); } /* DCT method */ label = gtk_label_new_with_mnemonic (_("_DCT method:")); gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); gtk_table_attach (GTK_TABLE (table), label, 2, 3, 3, 4, GTK_FILL, GTK_FILL, 0, 0); gtk_widget_show (label); pg.dct = combo = gimp_int_combo_box_new (_("Fast Integer"), 1, _("Integer"), 0, _("Floating-Point"), 2, NULL); gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (combo), jsvals.dct); gtk_table_attach (GTK_TABLE (table), combo, 3, 6, 3, 4, GTK_FILL | GTK_EXPAND, GTK_FILL, 0, 0); gtk_widget_show (combo); gtk_label_set_mnemonic_widget (GTK_LABEL (label), combo); g_signal_connect (combo, "changed", G_CALLBACK (gimp_int_combo_box_get_active), &jsvals.dct); g_signal_connect (combo, "changed", G_CALLBACK (make_preview), NULL); frame = gimp_frame_new (_("Comment")); gtk_box_pack_start (GTK_BOX (vbox), frame, TRUE, TRUE, 0); gtk_widget_show (frame); scrolled_window = gtk_scrolled_window_new (NULL, NULL); gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolled_window), GTK_SHADOW_IN); gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_widget_set_size_request (scrolled_window, 250, 50); gtk_container_add (GTK_CONTAINER (frame), scrolled_window); gtk_widget_show (scrolled_window); pg.text_buffer = text_buffer = gtk_text_buffer_new (NULL); if (image_comment) gtk_text_buffer_set_text (text_buffer, image_comment, -1); text_view = gtk_text_view_new_with_buffer (text_buffer); gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (text_view), GTK_WRAP_WORD); gtk_container_add (GTK_CONTAINER (scrolled_window), text_view); gtk_widget_show (text_view); g_object_unref (text_buffer); vbox = gtk_vbox_new (FALSE, 12); gtk_container_set_border_width (GTK_CONTAINER (vbox), 12); gtk_box_pack_start (GTK_BOX (gimp_export_dialog_get_content_area (dialog)), vbox, TRUE, TRUE, 0); gtk_widget_show (vbox); tabledefaults = gtk_table_new (1, 3, FALSE); gtk_table_set_col_spacings (GTK_TABLE (tabledefaults), 6); gtk_box_pack_start (GTK_BOX (vbox), tabledefaults, FALSE, FALSE, 0); gtk_widget_show (tabledefaults); button = gtk_button_new_with_mnemonic (_("_Load Defaults")); gtk_table_attach (GTK_TABLE (tabledefaults), button, 0, 1, 1, 2, GTK_FILL, 0, 0, 0); gtk_widget_show (button); g_signal_connect_swapped (button, "clicked", G_CALLBACK (load_gui_defaults), &pg); button = gtk_button_new_with_mnemonic (_("Sa_ve Defaults")); gtk_table_attach (GTK_TABLE (tabledefaults), button, 1, 2, 1, 2, GTK_FILL, 0, 0, 0); gtk_widget_show (button); g_signal_connect_swapped (button, "clicked", G_CALLBACK (save_defaults), &pg); gtk_widget_show (frame); gtk_widget_show (table); gtk_widget_show (dialog); make_preview (); pg.run = FALSE; gtk_main (); destroy_preview (); return pg.run; } static void save_dialog_response (GtkWidget *widget, gint response_id, gpointer data) { JpegSaveGui *pg = data; GtkTextIter start_iter; GtkTextIter end_iter; switch (response_id) { case GTK_RESPONSE_OK: gtk_text_buffer_get_bounds (pg->text_buffer, &start_iter, &end_iter); image_comment = gtk_text_buffer_get_text (pg->text_buffer, &start_iter, &end_iter, FALSE); pg->run = TRUE; /* fallthrough */ default: gtk_widget_destroy (widget); break; } } void load_save_defaults (void) { GimpParasite *parasite; gchar *def_str; JpegSaveVals tmpvals; gint num_fields; gint subsampling; jsvals.quality = DEFAULT_QUALITY; jsvals.smoothing = DEFAULT_SMOOTHING; jsvals.optimize = DEFAULT_OPTIMIZE; jsvals.progressive = DEFAULT_PROGRESSIVE; jsvals.baseline = DEFAULT_BASELINE; jsvals.subsmp = DEFAULT_SUBSMP; jsvals.restart = DEFAULT_RESTART; jsvals.dct = DEFAULT_DCT; jsvals.preview = DEFAULT_PREVIEW; jsvals.save_exif = DEFAULT_EXIF; jsvals.save_thumbnail = DEFAULT_THUMBNAIL; jsvals.save_xmp = DEFAULT_XMP; jsvals.use_orig_quality = DEFAULT_USE_ORIG_QUALITY; #ifdef HAVE_EXIF if (exif_data && (exif_data->data)) jsvals.save_thumbnail = TRUE; #endif /* HAVE_EXIF */ parasite = gimp_get_parasite (JPEG_DEFAULTS_PARASITE); if (! parasite) return; def_str = g_strndup (gimp_parasite_data (parasite), gimp_parasite_data_size (parasite)); gimp_parasite_free (parasite); num_fields = sscanf (def_str, "%lf %lf %d %d %d %d %d %d %d %d %d %d", &tmpvals.quality, &tmpvals.smoothing, &tmpvals.optimize, &tmpvals.progressive, &subsampling, &tmpvals.baseline, &tmpvals.restart, &tmpvals.dct, &tmpvals.preview, &tmpvals.save_exif, &tmpvals.save_thumbnail, &tmpvals.save_xmp); tmpvals.subsmp = subsampling; if (num_fields == 12) memcpy (&jsvals, &tmpvals, sizeof (tmpvals)); g_free (def_str); } static void save_defaults (void) { GimpParasite *parasite; gchar *def_str; def_str = g_strdup_printf ("%lf %lf %d %d %d %d %d %d %d %d %d %d", jsvals.quality, jsvals.smoothing, jsvals.optimize, jsvals.progressive, (gint) jsvals.subsmp, jsvals.baseline, jsvals.restart, jsvals.dct, jsvals.preview, jsvals.save_exif, jsvals.save_thumbnail, jsvals.save_xmp); parasite = gimp_parasite_new (JPEG_DEFAULTS_PARASITE, GIMP_PARASITE_PERSISTENT, strlen (def_str), def_str); gimp_attach_parasite (parasite); gimp_parasite_free (parasite); g_free (def_str); } static void load_gui_defaults (JpegSaveGui *pg) { GtkAdjustment *restart_markers; load_save_defaults (); #define SET_ACTIVE_BTTN(field) \ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (pg->field), jsvals.field) SET_ACTIVE_BTTN (optimize); SET_ACTIVE_BTTN (progressive); SET_ACTIVE_BTTN (use_orig_quality); SET_ACTIVE_BTTN (preview); #ifdef HAVE_EXIF SET_ACTIVE_BTTN (save_exif); SET_ACTIVE_BTTN (save_thumbnail); #endif SET_ACTIVE_BTTN (save_xmp); #undef SET_ACTIVE_BTTN /*spin button stuff*/ g_signal_handler_block (pg->use_restart_markers, pg->handler_id_restart); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (pg->use_restart_markers), jsvals.restart); restart_markers = GTK_ADJUSTMENT (pg->scale_data); gtk_adjustment_set_value (restart_markers, jsvals.restart); g_signal_handler_unblock (pg->use_restart_markers, pg->handler_id_restart); gtk_adjustment_set_value (GTK_ADJUSTMENT (pg->quality), jsvals.quality); gtk_adjustment_set_value (GTK_ADJUSTMENT (pg->smoothing), jsvals.smoothing); if (gimp_drawable_is_rgb (drawable_ID_global)) { gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (pg->subsmp), jsvals.subsmp); } gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (pg->dct), jsvals.dct); } static void save_restart_update (GtkAdjustment *adjustment, GtkWidget *toggle) { if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (toggle))) jsvals.restart = gtk_adjustment_get_value (adjustment); else jsvals.restart = 0; gtk_widget_set_sensitive (restart_markers_label, jsvals.restart); gtk_widget_set_sensitive (restart_markers_scale, jsvals.restart); make_preview (); } static void subsampling_changed (GtkWidget *combo, GtkObject *entry) { gint value; gimp_int_combo_box_get_active (GIMP_INT_COMBO_BOX (combo), &value); jsvals.subsmp = value; /* smoothing is not supported with nonstandard sampling ratios */ gimp_scale_entry_set_sensitive (entry, jsvals.subsmp != JPEG_SUPSAMPLING_2x1_1x1_1x1 && jsvals.subsmp != JPEG_SUPSAMPLING_1x2_1x1_1x1); make_preview (); } static void quality_changed (GtkObject *scale_entry, GtkWidget *toggle) { if (jsvals.use_orig_quality) { gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), FALSE); } } static void use_orig_qual_changed (GtkWidget *toggle, GtkObject *scale_entry) { if (jsvals.use_orig_quality && orig_quality > 0) { g_signal_handlers_block_by_func (scale_entry, quality_changed, toggle); gtk_adjustment_set_value (GTK_ADJUSTMENT (scale_entry), orig_quality); g_signal_handlers_unblock_by_func (scale_entry, quality_changed, toggle); } } static void use_orig_qual_changed2 (GtkWidget *toggle, GtkWidget *combo) { /* the test is (orig_quality > 0), not (orig_subsmp > 0) - this is normal */ if (jsvals.use_orig_quality && orig_quality > 0) { gimp_int_combo_box_set_active (GIMP_INT_COMBO_BOX (combo), orig_subsmp); } } #ifdef HAVE_EXIF static guchar *tbuffer = NULL; static guchar *tbuffer2 = NULL; static gint tbuffer_count = 0; typedef struct { struct jpeg_destination_mgr pub; /* public fields */ guchar *buffer; gint size; } my_destination_mgr; typedef my_destination_mgr *my_dest_ptr; static void init_destination (j_compress_ptr cinfo) { } static gboolean empty_output_buffer (j_compress_ptr cinfo) { my_dest_ptr dest = (my_dest_ptr) cinfo->dest; tbuffer_count = tbuffer_count + 16384; tbuffer = g_renew (guchar, tbuffer, tbuffer_count); g_memmove (tbuffer + tbuffer_count - 16384, tbuffer2, 16384); dest->pub.next_output_byte = tbuffer2; dest->pub.free_in_buffer = 16384; return TRUE; } static void term_destination (j_compress_ptr cinfo) { my_dest_ptr dest = (my_dest_ptr) cinfo->dest; tbuffer_count = (tbuffer_count + 16384) - (dest->pub.free_in_buffer); tbuffer = g_renew (guchar, tbuffer, tbuffer_count); g_memmove (tbuffer + tbuffer_count - (16384 - dest->pub.free_in_buffer), tbuffer2, 16384 - dest->pub.free_in_buffer); } static gint create_thumbnail (gint32 image_ID, gint32 drawable_ID, gdouble quality, guchar **thumbnail_buffer) { GimpDrawable *drawable; gint req_width, req_height, bpp, rbpp; guchar *thumbnail_data = NULL; struct jpeg_compress_struct cinfo; struct my_error_mgr jerr; my_dest_ptr dest; gboolean alpha = FALSE; JSAMPROW scanline[1]; guchar *buf = NULL; gint i; drawable = gimp_drawable_get (drawable_ID); req_width = 196; req_height = 196; if (MIN (drawable->width, drawable->height) < 196) req_width = req_height = MIN(drawable->width, drawable->height); thumbnail_data = gimp_drawable_get_thumbnail_data (drawable_ID, &req_width, &req_height, &bpp); if (! thumbnail_data) return 0; rbpp = bpp; if ((bpp == 2) || (bpp == 4)) { alpha = TRUE; rbpp = bpp - 1; } buf = g_new (guchar, req_width * bpp); tbuffer2 = g_new (guchar, 16384); tbuffer_count = 0; cinfo.err = jpeg_std_error (&jerr.pub); jerr.pub.error_exit = my_error_exit; /* Establish the setjmp return context for my_error_exit to use. */ if (setjmp (jerr.setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error. * We need to clean up the JPEG object, free memory, and return. */ jpeg_destroy_compress (&cinfo); if (thumbnail_data) { g_free (thumbnail_data); thumbnail_data = NULL; } if (buf) { g_free (buf); buf = NULL; } if (tbuffer2) { g_free (tbuffer2); tbuffer2 = NULL; } if (drawable) gimp_drawable_detach (drawable); return 0; } /* Now we can initialize the JPEG compression object. */ jpeg_create_compress (&cinfo); if (cinfo.dest == NULL) cinfo.dest = (struct jpeg_destination_mgr *) (*cinfo.mem->alloc_small) ((j_common_ptr) &cinfo, JPOOL_PERMANENT, sizeof(my_destination_mgr)); dest = (my_dest_ptr) cinfo.dest; dest->pub.init_destination = init_destination; dest->pub.empty_output_buffer = empty_output_buffer; dest->pub.term_destination = term_destination; dest->pub.next_output_byte = tbuffer2; dest->pub.free_in_buffer = 16384; dest->buffer = tbuffer2; dest->size = 16384; cinfo.input_components = rbpp; cinfo.image_width = req_width; cinfo.image_height = req_height; /* colorspace of input image */ cinfo.in_color_space = (rbpp == 3) ? JCS_RGB : JCS_GRAYSCALE; /* Now use the library's routine to set default compression parameters. * (You must set at least cinfo.in_color_space before calling this, * since the defaults depend on the source color space.) */ jpeg_set_defaults (&cinfo); jpeg_set_quality (&cinfo, (gint) (quality + 0.5), jsvals.baseline); /* Step 4: Start compressor */ /* TRUE ensures that we will write a complete interchange-JPEG file. * Pass TRUE unless you are very sure of what you're doing. */ jpeg_start_compress (&cinfo, TRUE); while (cinfo.next_scanline < (unsigned int) req_height) { for (i = 0; i < req_width; i++) { buf[(i * rbpp) + 0] = thumbnail_data[(cinfo.next_scanline * req_width * bpp) + (i * bpp) + 0]; if (rbpp == 3) { buf[(i * rbpp) + 1] = thumbnail_data[(cinfo.next_scanline * req_width * bpp) + (i * bpp) + 1]; buf[(i * rbpp) + 2] = thumbnail_data[(cinfo.next_scanline * req_width * bpp) + (i * bpp) + 2]; } } scanline[0] = buf; jpeg_write_scanlines (&cinfo, scanline, 1); } /* Step 6: Finish compression */ jpeg_finish_compress (&cinfo); /* Step 7: release JPEG compression object */ /* This is an important step since it will release a good deal of memory. */ jpeg_destroy_compress (&cinfo); /* And we're done! */ if (thumbnail_data) { g_free (thumbnail_data); thumbnail_data = NULL; } if (buf) { g_free (buf); buf = NULL; } if (drawable) { gimp_drawable_detach (drawable); drawable = NULL; } *thumbnail_buffer = tbuffer; return tbuffer_count; } #endif /* HAVE_EXIF */