2006-12-10 05:33:38 +08:00
|
|
|
/* GIMP - The GNU Image Manipulation Program
|
2005-01-04 05:36:43 +08:00
|
|
|
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
|
|
|
|
*
|
2009-01-18 06:28:01 +08:00
|
|
|
* This program is free software: you can redistribute it and/or modify
|
2005-01-04 05:36:43 +08:00
|
|
|
* it under the terms of the GNU General Public License as published by
|
2009-01-18 06:28:01 +08:00
|
|
|
* the Free Software Foundation; either version 3 of the License, or
|
2005-01-04 05:36:43 +08:00
|
|
|
* (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
|
2018-07-12 05:27:07 +08:00
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
2005-01-04 05:36:43 +08:00
|
|
|
*/
|
|
|
|
|
2005-01-07 07:21:43 +08:00
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <setjmp.h>
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
#include <jpeglib.h>
|
|
|
|
#include <jerror.h>
|
|
|
|
|
|
|
|
#include <libgimp/gimp.h>
|
|
|
|
#include <libgimp/gimpui.h>
|
|
|
|
|
|
|
|
#include "libgimp/stdplugins-intl.h"
|
|
|
|
|
2005-01-04 05:36:43 +08:00
|
|
|
#include "jpeg.h"
|
2007-07-27 07:01:21 +08:00
|
|
|
#include "jpeg-settings.h"
|
2005-01-04 05:36:43 +08:00
|
|
|
#include "jpeg-load.h"
|
2005-01-07 07:21:43 +08:00
|
|
|
#include "jpeg-save.h"
|
2005-08-12 23:46:46 +08:00
|
|
|
|
2005-01-07 07:21:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
typedef struct _Jpeg Jpeg;
|
|
|
|
typedef struct _JpegClass JpegClass;
|
|
|
|
|
|
|
|
struct _Jpeg
|
|
|
|
{
|
|
|
|
GimpPlugIn parent_instance;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct _JpegClass
|
|
|
|
{
|
|
|
|
GimpPlugInClass parent_class;
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
#define JPEG_TYPE (jpeg_get_type ())
|
|
|
|
#define JPEG (obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), JPEG_TYPE, Jpeg))
|
|
|
|
|
|
|
|
GType jpeg_get_type (void) G_GNUC_CONST;
|
|
|
|
|
|
|
|
static GList * jpeg_query_procedures (GimpPlugIn *plug_in);
|
|
|
|
static GimpProcedure * jpeg_create_procedure (GimpPlugIn *plug_in,
|
2019-10-20 20:32:02 +08:00
|
|
|
const gchar *name);
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
static GimpValueArray * jpeg_load (GimpProcedure *procedure,
|
2019-10-20 20:32:02 +08:00
|
|
|
GimpRunMode run_mode,
|
|
|
|
GFile *file,
|
|
|
|
const GimpValueArray *args,
|
|
|
|
gpointer run_data);
|
2019-08-24 08:33:45 +08:00
|
|
|
static GimpValueArray * jpeg_load_thumb (GimpProcedure *procedure,
|
2019-10-20 20:32:02 +08:00
|
|
|
GFile *file,
|
|
|
|
gint size,
|
|
|
|
const GimpValueArray *args,
|
|
|
|
gpointer run_data);
|
2019-08-24 08:33:45 +08:00
|
|
|
static GimpValueArray * jpeg_save (GimpProcedure *procedure,
|
2019-10-20 20:32:02 +08:00
|
|
|
GimpRunMode run_mode,
|
|
|
|
GimpImage *image,
|
2020-04-14 17:46:17 +08:00
|
|
|
gint n_drawables,
|
|
|
|
GimpDrawable **drawables,
|
2019-10-20 20:32:02 +08:00
|
|
|
GFile *file,
|
|
|
|
const GimpValueArray *args,
|
|
|
|
gpointer run_data);
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
G_DEFINE_TYPE (Jpeg, jpeg, GIMP_TYPE_PLUG_IN)
|
|
|
|
|
|
|
|
GIMP_MAIN (JPEG_TYPE)
|
2022-05-26 06:59:36 +08:00
|
|
|
DEFINE_STD_SET_I18N
|
2019-08-24 08:33:45 +08:00
|
|
|
|
2005-01-07 07:21:43 +08:00
|
|
|
|
2019-10-20 20:32:02 +08:00
|
|
|
gboolean undo_touched = FALSE;
|
|
|
|
GimpDisplay *display = NULL;
|
2021-04-30 07:31:18 +08:00
|
|
|
gboolean separate_display = FALSE;
|
2019-10-20 20:32:02 +08:00
|
|
|
GimpImage *orig_image_global = NULL;
|
|
|
|
GimpDrawable *drawable_global = NULL;
|
2007-08-11 05:45:38 +08:00
|
|
|
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
static void
|
|
|
|
jpeg_class_init (JpegClass *klass)
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
plug_in_class->query_procedures = jpeg_query_procedures;
|
|
|
|
plug_in_class->create_procedure = jpeg_create_procedure;
|
2022-05-26 06:59:36 +08:00
|
|
|
plug_in_class->set_i18n = STD_SET_I18N;
|
2019-08-24 08:33:45 +08:00
|
|
|
}
|
2005-01-04 05:36:43 +08:00
|
|
|
|
|
|
|
static void
|
2019-08-24 08:33:45 +08:00
|
|
|
jpeg_init (Jpeg *jpeg)
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
|
|
|
}
|
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
static GList *
|
|
|
|
jpeg_query_procedures (GimpPlugIn *plug_in)
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
GList *list = NULL;
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
list = g_list_append (list, g_strdup (LOAD_THUMB_PROC));
|
2019-09-11 01:36:54 +08:00
|
|
|
list = g_list_append (list, g_strdup (LOAD_PROC));
|
2019-08-24 08:33:45 +08:00
|
|
|
list = g_list_append (list, g_strdup (SAVE_PROC));
|
|
|
|
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
static GimpProcedure *
|
|
|
|
jpeg_create_procedure (GimpPlugIn *plug_in,
|
2019-10-20 20:32:02 +08:00
|
|
|
const gchar *name)
|
2019-08-24 08:33:45 +08:00
|
|
|
{
|
|
|
|
GimpProcedure *procedure = NULL;
|
|
|
|
|
|
|
|
if (! strcmp (name, LOAD_PROC))
|
|
|
|
{
|
2019-08-30 18:52:28 +08:00
|
|
|
procedure = gimp_load_procedure_new (plug_in, name,
|
|
|
|
GIMP_PDB_PROC_TYPE_PLUGIN,
|
2019-08-24 08:33:45 +08:00
|
|
|
jpeg_load, NULL, NULL);
|
|
|
|
|
2022-07-05 04:50:53 +08:00
|
|
|
gimp_procedure_set_menu_label (procedure, _("JPEG image"));
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
gimp_procedure_set_documentation (procedure,
|
|
|
|
"Loads files in the JPEG file format",
|
|
|
|
"Loads files in the JPEG file format",
|
|
|
|
name);
|
|
|
|
gimp_procedure_set_attribution (procedure,
|
|
|
|
"Spencer Kimball, Peter Mattis & others",
|
|
|
|
"Spencer Kimball & Peter Mattis",
|
|
|
|
"1995-2007");
|
|
|
|
|
|
|
|
gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure),
|
|
|
|
"image/jpeg");
|
|
|
|
gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
|
|
|
|
"jpg,jpeg,jpe");
|
|
|
|
gimp_file_procedure_set_magics (GIMP_FILE_PROCEDURE (procedure),
|
2020-08-26 18:13:29 +08:00
|
|
|
"0,string,\xff\xd8\xff");
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
gimp_load_procedure_set_thumbnail_loader (GIMP_LOAD_PROCEDURE (procedure),
|
|
|
|
LOAD_THUMB_PROC);
|
|
|
|
}
|
|
|
|
else if (! strcmp (name, LOAD_THUMB_PROC))
|
|
|
|
{
|
2019-08-30 18:52:28 +08:00
|
|
|
procedure = gimp_thumbnail_procedure_new (plug_in, name,
|
|
|
|
GIMP_PDB_PROC_TYPE_PLUGIN,
|
2019-08-24 08:33:45 +08:00
|
|
|
jpeg_load_thumb, NULL, NULL);
|
|
|
|
|
|
|
|
gimp_procedure_set_documentation (procedure,
|
|
|
|
"Loads a thumbnail from a JPEG image",
|
|
|
|
"Loads a thumbnail from a JPEG image, "
|
|
|
|
"if one exists",
|
|
|
|
name);
|
|
|
|
gimp_procedure_set_attribution (procedure,
|
|
|
|
"Mukund Sivaraman <muks@mukund.org>, "
|
|
|
|
"Sven Neumann <sven@gimp.org>",
|
|
|
|
"Mukund Sivaraman <muks@mukund.org>, "
|
|
|
|
"Sven Neumann <sven@gimp.org>",
|
|
|
|
"November 15, 2004");
|
|
|
|
}
|
|
|
|
else if (! strcmp (name, SAVE_PROC))
|
|
|
|
{
|
2019-08-30 18:52:28 +08:00
|
|
|
procedure = gimp_save_procedure_new (plug_in, name,
|
|
|
|
GIMP_PDB_PROC_TYPE_PLUGIN,
|
2019-08-24 08:33:45 +08:00
|
|
|
jpeg_save, NULL, NULL);
|
|
|
|
|
|
|
|
gimp_procedure_set_image_types (procedure, "RGB*, GRAY*");
|
|
|
|
|
2022-07-05 04:50:53 +08:00
|
|
|
gimp_procedure_set_menu_label (procedure, _("JPEG image"));
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
gimp_procedure_set_documentation (procedure,
|
|
|
|
"Saves files in the JPEG file format",
|
|
|
|
"Saves files in the lossy, widely "
|
|
|
|
"supported JPEG format",
|
|
|
|
name);
|
|
|
|
gimp_procedure_set_attribution (procedure,
|
|
|
|
"Spencer Kimball, Peter Mattis & others",
|
|
|
|
"Spencer Kimball & Peter Mattis",
|
|
|
|
"1995-2007");
|
|
|
|
|
2020-12-17 09:02:14 +08:00
|
|
|
gimp_file_procedure_set_format_name (GIMP_FILE_PROCEDURE (procedure),
|
|
|
|
_("JPEG"));
|
2019-08-24 08:33:45 +08:00
|
|
|
gimp_file_procedure_set_mime_types (GIMP_FILE_PROCEDURE (procedure),
|
|
|
|
"image/jpeg");
|
|
|
|
gimp_file_procedure_set_extensions (GIMP_FILE_PROCEDURE (procedure),
|
|
|
|
"jpg,jpeg,jpe");
|
|
|
|
|
2019-10-20 20:32:02 +08:00
|
|
|
/* See bugs #63610 and #61088 for a discussion about the quality
|
|
|
|
* settings
|
|
|
|
*/
|
2019-08-24 08:33:45 +08:00
|
|
|
GIMP_PROC_ARG_DOUBLE (procedure, "quality",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("_Quality"),
|
|
|
|
_("Quality of exported image"),
|
2019-08-24 08:33:45 +08:00
|
|
|
0.0, 1.0, 0.9,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
GIMP_PROC_ARG_DOUBLE (procedure, "smoothing",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("S_moothing"),
|
|
|
|
_("Smoothing factor for exported image"),
|
2019-08-24 08:33:45 +08:00
|
|
|
0.0, 1.0, 0.0,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
GIMP_PROC_ARG_BOOLEAN (procedure, "optimize",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("Optimi_ze"),
|
|
|
|
_("Use optimized tables during Huffman coding"),
|
2019-08-24 08:33:45 +08:00
|
|
|
TRUE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
GIMP_PROC_ARG_BOOLEAN (procedure, "progressive",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("_Progressive"),
|
|
|
|
_("Create progressive JPEG images"),
|
2019-08-24 08:33:45 +08:00
|
|
|
TRUE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
plug-ins: support CMYK import/export for JPEG.
We already had import support through littleCMS. We now use fully
babl/GEGL which makes our code more straightforward and identical,
whichever the input format.
The export support is totally new. It comes with a checkbox to propose
selecting CMYK export and a label displaying the CMYK profile which will
be used.
Now this whole implementation has a few drawbacks so far, but it will be
a good first sample for future CMYK-related improvements to come:
* The export profile I am using is what we call the "simulation
profile" from the GimpColorConfig. This corresponds to the default
"Soft-proofing" profile as set in Preferences. In particular, this is
not the actual soft-proofing profile for this image which might have
been changed through the View menu because this information is
currently and unfortunately unavailable to plug-ins. It is not the
"Preferred CMYK Profile" either, as set in Preferences.
TODOS:
- We really need to straighten the soft-proof profile core concept by
storing it in the image and making it visible to plug-in.
- Another interesting improvement could be to create a
GimpColorProfile procedure argument which would be mapped to a color
profile chooser widget, allowing people to choose profiles in
plug-ins. For an export plug-in in particular, it could allow to
select a profile different from the soft-proof one at export time.
* When we export, if no profile is choosen, babl will use a naive
profile. It would be nice to store this naive profile into the JPEG if
the "Save color profile" option is checked (same as we store a generic
sRGB profile when no RGB profile is set).
* When we import, we just import the image as sRGB. Since CMYK gamuts
are not necessarily within sRGB (some part of the spectrum is usually
well within, but other well outside), other than the basic conversion
accuracy issue, we may lose colors. It would be much nicer to be able
to select an output RGB profile. Optionally if we could create a RGB
color space which is made to contain the whole input CMYK profile
color space, without explicit choice step, it would be nice too.
* I am using babl's "cmyk" format, not the expected "CMYK" format.
"cmyk" is meant to be an inverted CMYK where 0.0 is full ink coverage
and 1.0 none. Nevertheless when loading the resulting JPEG in other
software (editors or viewers alike), the normal CMYK would always
display inverted colors and the inverted cmyk would look fine.
Finally I found a docs from libjpeg-turbo library, explaining that
Photoshop was wrongly inverting CMYK color data while it should not.
This text dates back from 1994, looking at the commit date which
introduced this paragraph. In the 28 years since then, could this
color inversion have become the de-facto standard for JPEG because one
of the main editor would just output all its JPEG files this way?
See: https://github.com/libjpeg-turbo/libjpeg-turbo/blob/dfc63d42ee3d1ae8eacb921e89e64ac57861dff6/libjpeg.txt#L1425-L1438
2022-04-18 02:26:41 +08:00
|
|
|
GIMP_PROC_ARG_BOOLEAN (procedure, "cmyk",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("Export as CM_YK"),
|
|
|
|
_("Create a CMYK JPEG image using the soft-proofing color profile"),
|
plug-ins: support CMYK import/export for JPEG.
We already had import support through littleCMS. We now use fully
babl/GEGL which makes our code more straightforward and identical,
whichever the input format.
The export support is totally new. It comes with a checkbox to propose
selecting CMYK export and a label displaying the CMYK profile which will
be used.
Now this whole implementation has a few drawbacks so far, but it will be
a good first sample for future CMYK-related improvements to come:
* The export profile I am using is what we call the "simulation
profile" from the GimpColorConfig. This corresponds to the default
"Soft-proofing" profile as set in Preferences. In particular, this is
not the actual soft-proofing profile for this image which might have
been changed through the View menu because this information is
currently and unfortunately unavailable to plug-ins. It is not the
"Preferred CMYK Profile" either, as set in Preferences.
TODOS:
- We really need to straighten the soft-proof profile core concept by
storing it in the image and making it visible to plug-in.
- Another interesting improvement could be to create a
GimpColorProfile procedure argument which would be mapped to a color
profile chooser widget, allowing people to choose profiles in
plug-ins. For an export plug-in in particular, it could allow to
select a profile different from the soft-proof one at export time.
* When we export, if no profile is choosen, babl will use a naive
profile. It would be nice to store this naive profile into the JPEG if
the "Save color profile" option is checked (same as we store a generic
sRGB profile when no RGB profile is set).
* When we import, we just import the image as sRGB. Since CMYK gamuts
are not necessarily within sRGB (some part of the spectrum is usually
well within, but other well outside), other than the basic conversion
accuracy issue, we may lose colors. It would be much nicer to be able
to select an output RGB profile. Optionally if we could create a RGB
color space which is made to contain the whole input CMYK profile
color space, without explicit choice step, it would be nice too.
* I am using babl's "cmyk" format, not the expected "CMYK" format.
"cmyk" is meant to be an inverted CMYK where 0.0 is full ink coverage
and 1.0 none. Nevertheless when loading the resulting JPEG in other
software (editors or viewers alike), the normal CMYK would always
display inverted colors and the inverted cmyk would look fine.
Finally I found a docs from libjpeg-turbo library, explaining that
Photoshop was wrongly inverting CMYK color data while it should not.
This text dates back from 1994, looking at the commit date which
introduced this paragraph. In the 28 years since then, could this
color inversion have become the de-facto standard for JPEG because one
of the main editor would just output all its JPEG files this way?
See: https://github.com/libjpeg-turbo/libjpeg-turbo/blob/dfc63d42ee3d1ae8eacb921e89e64ac57861dff6/libjpeg.txt#L1425-L1438
2022-04-18 02:26:41 +08:00
|
|
|
FALSE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
GIMP_PROC_ARG_INT (procedure, "sub-sampling",
|
2020-11-26 05:44:43 +08:00
|
|
|
_("Su_bsampling"),
|
2023-08-14 10:34:03 +08:00
|
|
|
_("Sub-sampling type { 0 == 4:2:0 (chroma quartered), "
|
|
|
|
"1 == 4:2:2 (chroma halved horizontally), "
|
|
|
|
"2 == 4:4:4 (best quality), "
|
|
|
|
"3 == 4:4:0 (chroma halved vertically)"),
|
2019-08-24 08:33:45 +08:00
|
|
|
JPEG_SUBSAMPLING_2x2_1x1_1x1,
|
|
|
|
JPEG_SUBSAMPLING_1x2_1x1_1x1,
|
|
|
|
JPEG_SUBSAMPLING_1x1_1x1_1x1,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
GIMP_PROC_ARG_BOOLEAN (procedure, "baseline",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("Baseline"),
|
|
|
|
_("Force creation of a baseline JPEG "
|
|
|
|
"(non-baseline JPEGs can't be read by all decoders)"),
|
2019-08-24 08:33:45 +08:00
|
|
|
TRUE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
GIMP_PROC_ARG_INT (procedure, "restart",
|
2020-11-26 08:37:17 +08:00
|
|
|
_("Inter_val (MCU rows):"),
|
2023-08-14 10:34:03 +08:00
|
|
|
_("Interval of restart markers "
|
|
|
|
"(in MCU rows, 0 = no restart markers)"),
|
2019-08-24 08:33:45 +08:00
|
|
|
0, 64, 0,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
GIMP_PROC_ARG_INT (procedure, "dct",
|
2020-11-26 05:44:43 +08:00
|
|
|
_("_DCT method"),
|
2023-08-14 10:34:03 +08:00
|
|
|
_("DCT method to use { INTEGER (0), FIXED (1), "
|
|
|
|
"FLOAT (2) }"),
|
2019-08-24 08:33:45 +08:00
|
|
|
0, 2, 0,
|
|
|
|
G_PARAM_READWRITE);
|
2020-11-26 05:44:43 +08:00
|
|
|
|
|
|
|
/* Some auxiliary arguments mostly for interactive usage. */
|
|
|
|
|
|
|
|
GIMP_PROC_AUX_ARG_BOOLEAN (procedure, "use-original-quality",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("_Use quality settings from original image"),
|
|
|
|
_("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."),
|
2020-11-26 05:44:43 +08:00
|
|
|
FALSE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
GIMP_PROC_AUX_ARG_INT (procedure, "original-quality",
|
|
|
|
NULL, NULL,
|
|
|
|
-1, 100, -1,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
GIMP_PROC_AUX_ARG_INT (procedure, "original-sub-sampling",
|
|
|
|
NULL, NULL,
|
|
|
|
JPEG_SUBSAMPLING_2x2_1x1_1x1,
|
|
|
|
JPEG_SUBSAMPLING_1x2_1x1_1x1,
|
|
|
|
JPEG_SUBSAMPLING_2x2_1x1_1x1,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
GIMP_PROC_AUX_ARG_INT (procedure, "original-num-quant-tables",
|
|
|
|
NULL, NULL,
|
|
|
|
-1, 4, -1,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
GIMP_PROC_AUX_ARG_BOOLEAN (procedure, "show-preview",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("Sho_w preview in image window"),
|
|
|
|
_("Creates a temporary layer with an export preview"),
|
2020-11-26 05:44:43 +08:00
|
|
|
FALSE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
GIMP_PROC_AUX_ARG_BOOLEAN (procedure, "use-arithmetic-coding",
|
2023-08-14 10:34:03 +08:00
|
|
|
_("Use _arithmetic coding"),
|
2020-11-26 05:44:43 +08:00
|
|
|
_("Older software may have trouble opening "
|
|
|
|
"arithmetic-coded images"),
|
|
|
|
FALSE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
GIMP_PROC_AUX_ARG_BOOLEAN (procedure, "use-restart",
|
2020-11-26 08:37:17 +08:00
|
|
|
_("Use restart mar_kers"),
|
2020-11-26 05:44:43 +08:00
|
|
|
NULL, FALSE,
|
|
|
|
G_PARAM_READWRITE);
|
|
|
|
|
|
|
|
gimp_save_procedure_set_support_exif (GIMP_SAVE_PROCEDURE (procedure), TRUE);
|
|
|
|
gimp_save_procedure_set_support_iptc (GIMP_SAVE_PROCEDURE (procedure), TRUE);
|
|
|
|
gimp_save_procedure_set_support_xmp (GIMP_SAVE_PROCEDURE (procedure), TRUE);
|
|
|
|
gimp_save_procedure_set_support_profile (GIMP_SAVE_PROCEDURE (procedure), TRUE);
|
|
|
|
gimp_save_procedure_set_support_thumbnail (GIMP_SAVE_PROCEDURE (procedure), TRUE);
|
|
|
|
gimp_save_procedure_set_support_comment (GIMP_SAVE_PROCEDURE (procedure), TRUE);
|
2019-08-24 08:33:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return procedure;
|
|
|
|
}
|
|
|
|
|
|
|
|
static GimpValueArray *
|
|
|
|
jpeg_load (GimpProcedure *procedure,
|
2019-10-20 20:32:02 +08:00
|
|
|
GimpRunMode run_mode,
|
|
|
|
GFile *file,
|
|
|
|
const GimpValueArray *args,
|
|
|
|
gpointer run_data)
|
2019-08-24 08:33:45 +08:00
|
|
|
{
|
|
|
|
GimpValueArray *return_vals;
|
|
|
|
GimpImage *image;
|
2023-03-06 10:48:30 +08:00
|
|
|
gboolean resolution_loaded = FALSE;
|
|
|
|
gboolean ps_metadata_loaded = FALSE;
|
|
|
|
GError *error = NULL;
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2013-05-13 14:09:45 +08:00
|
|
|
gegl_init (NULL, NULL);
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
preview_image = NULL;
|
|
|
|
preview_layer = NULL;
|
2007-08-08 18:31:38 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
switch (run_mode)
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
case GIMP_RUN_INTERACTIVE:
|
|
|
|
case GIMP_RUN_WITH_LAST_VALS:
|
2019-09-21 01:39:00 +08:00
|
|
|
gimp_ui_init (PLUG_IN_BINARY);
|
2019-08-24 08:33:45 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-09-12 03:48:34 +08:00
|
|
|
image = load_image (file, run_mode, FALSE,
|
2023-03-06 10:48:30 +08:00
|
|
|
&resolution_loaded, &ps_metadata_loaded, &error);
|
2019-08-19 18:05:12 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
if (image)
|
|
|
|
{
|
|
|
|
GimpMetadata *metadata;
|
2013-10-28 01:52:46 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
metadata = gimp_image_metadata_load_prepare (image, "image/jpeg",
|
|
|
|
file, NULL);
|
|
|
|
|
|
|
|
if (metadata)
|
2005-01-05 01:48:13 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
GimpMetadataLoadFlags flags = GIMP_METADATA_LOAD_ALL;
|
|
|
|
|
|
|
|
if (resolution_loaded)
|
|
|
|
flags &= ~GIMP_METADATA_LOAD_RESOLUTION;
|
|
|
|
|
|
|
|
gimp_image_metadata_load_finish (image, "image/jpeg",
|
app, libgimp, plug-ins: move Orientation metadata handling into core.
Orientation is now handled by core code, just next to profile conversion
handling.
One of the first consequence is that we don't need to have a non-GUI
version gimp_image_metadata_load_finish_batch() in libgimp, next to a
GUI version of the gimp_image_metadata_load_finish() function in
libgimpui. This makes for simpler API.
Also a plug-in which wishes to get access to the rotation dialog
provided by GIMP without loading ligimpui/GTK+ (for whatever reason)
will still have the feature.
The main advantage is that the "Don't ask me again" feature is now
handled by a settings in `Preferences > Image Import & Export` as the
"Metadata rotation policy". Until now it was saved as a global parasite,
which made it virtually non-editable once you checked it once (no easy
way to edit parasites except by scripts). So say you refused the
rotation once while checking "Don't ask again", and GIMP will forever
discard the rotation metadata without giving you a sane way to change
your mind. Of course, I could have passed the settings to plug-ins
through the PDB, but I find it a lot better to simply handle such
settings core-side.
The dialog code is basically the same as an app/dialogs/ as it was in
libgimp, with the minor improvement that it now takes the scale ratio
into account (basically the maximum thumbnail size will be bigger on
higher density displays).
Only downside of the move to the core is that this rotation dialog is
raised only when you open an image from the core, not as a PDB call. So
a plug-in which makes say a "file-jpeg-load" PDB call, even in
INTERACTIVE run mode, won't have rotation processed. Note that this was
already the same for embedded color profile conversion. This can be
wanted or not. Anyway some additional libgimp calls might be of interest
to explicitly call the core dialogs.
2020-09-24 01:59:09 +08:00
|
|
|
metadata, flags);
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
g_object_unref (metadata);
|
2005-01-05 01:48:13 +08:00
|
|
|
}
|
2019-08-24 08:33:45 +08:00
|
|
|
}
|
2005-01-05 01:48:13 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
if (! image)
|
|
|
|
return gimp_procedure_new_return_values (procedure,
|
|
|
|
GIMP_PDB_EXECUTION_ERROR,
|
|
|
|
error);
|
2013-10-27 22:22:35 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
return_vals = gimp_procedure_new_return_values (procedure,
|
|
|
|
GIMP_PDB_SUCCESS,
|
|
|
|
NULL);
|
2013-10-27 22:22:35 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
GIMP_VALUES_SET_IMAGE (return_vals, 1, image);
|
2013-10-27 22:22:35 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
return return_vals;
|
|
|
|
}
|
2013-10-28 01:52:46 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
static GimpValueArray *
|
|
|
|
jpeg_load_thumb (GimpProcedure *procedure,
|
2019-10-20 20:32:02 +08:00
|
|
|
GFile *file,
|
|
|
|
gint size,
|
|
|
|
const GimpValueArray *args,
|
|
|
|
gpointer run_data)
|
2019-08-24 08:33:45 +08:00
|
|
|
{
|
|
|
|
GimpValueArray *return_vals;
|
|
|
|
GimpImage *image;
|
|
|
|
gint width = 0;
|
|
|
|
gint height = 0;
|
|
|
|
GimpImageType type = -1;
|
|
|
|
GError *error = NULL;
|
2013-10-20 00:38:01 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
gegl_init (NULL, NULL);
|
2013-10-20 00:38:01 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
preview_image = NULL;
|
|
|
|
preview_layer = NULL;
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
image = load_thumbnail_image (file, &width, &height, &type,
|
|
|
|
&error);
|
2013-10-20 00:38:01 +08:00
|
|
|
|
2014-06-05 08:07:13 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
if (! image)
|
|
|
|
return gimp_procedure_new_return_values (procedure,
|
|
|
|
GIMP_PDB_EXECUTION_ERROR,
|
|
|
|
error);
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
return_vals = gimp_procedure_new_return_values (procedure,
|
|
|
|
GIMP_PDB_SUCCESS,
|
|
|
|
NULL);
|
2014-06-06 03:33:40 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
GIMP_VALUES_SET_IMAGE (return_vals, 1, image);
|
|
|
|
GIMP_VALUES_SET_INT (return_vals, 2, width);
|
|
|
|
GIMP_VALUES_SET_INT (return_vals, 3, height);
|
|
|
|
GIMP_VALUES_SET_ENUM (return_vals, 4, type);
|
|
|
|
GIMP_VALUES_SET_INT (return_vals, 5, 1); /* 1 layer */
|
2013-11-10 07:18:48 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
return return_vals;
|
|
|
|
}
|
2013-11-10 07:18:48 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
static GimpValueArray *
|
|
|
|
jpeg_save (GimpProcedure *procedure,
|
2019-10-20 20:32:02 +08:00
|
|
|
GimpRunMode run_mode,
|
|
|
|
GimpImage *image,
|
2020-04-14 17:46:17 +08:00
|
|
|
gint n_drawables,
|
|
|
|
GimpDrawable **drawables,
|
2019-10-20 20:32:02 +08:00
|
|
|
GFile *file,
|
|
|
|
const GimpValueArray *args,
|
|
|
|
gpointer run_data)
|
2019-08-24 08:33:45 +08:00
|
|
|
{
|
|
|
|
GimpPDBStatusType status = GIMP_PDB_SUCCESS;
|
2020-11-26 05:44:43 +08:00
|
|
|
GimpProcedureConfig *config;
|
2019-08-24 08:33:45 +08:00
|
|
|
GimpMetadata *metadata;
|
|
|
|
GimpImage *orig_image;
|
|
|
|
GimpExportReturn export = GIMP_EXPORT_CANCEL;
|
|
|
|
GError *error = NULL;
|
|
|
|
|
2020-11-26 05:44:43 +08:00
|
|
|
gint orig_num_quant_tables = -1;
|
|
|
|
gint orig_quality = -1;
|
|
|
|
JpegSubsampling orig_subsmp = JPEG_SUBSAMPLING_2x2_1x1_1x1;
|
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
gegl_init (NULL, NULL);
|
|
|
|
|
2020-11-26 05:44:43 +08:00
|
|
|
config = gimp_procedure_create_config (procedure);
|
|
|
|
metadata = gimp_procedure_config_begin_export (config, image, run_mode,
|
|
|
|
args, "image/jpeg");
|
2019-08-24 08:33:45 +08:00
|
|
|
preview_image = NULL;
|
|
|
|
preview_layer = NULL;
|
|
|
|
|
|
|
|
orig_image = image;
|
|
|
|
|
|
|
|
switch (run_mode)
|
|
|
|
{
|
|
|
|
case GIMP_RUN_INTERACTIVE:
|
|
|
|
case GIMP_RUN_WITH_LAST_VALS:
|
2019-09-21 01:39:00 +08:00
|
|
|
gimp_ui_init (PLUG_IN_BINARY);
|
2019-08-24 08:33:45 +08:00
|
|
|
|
2020-04-14 17:46:17 +08:00
|
|
|
export = gimp_export_image (&image, &n_drawables, &drawables, "JPEG",
|
2019-08-24 08:33:45 +08:00
|
|
|
GIMP_EXPORT_CAN_HANDLE_RGB |
|
|
|
|
GIMP_EXPORT_CAN_HANDLE_GRAY);
|
|
|
|
|
|
|
|
switch (export)
|
|
|
|
{
|
|
|
|
case GIMP_EXPORT_EXPORT:
|
2023-02-14 05:33:35 +08:00
|
|
|
display = NULL;
|
|
|
|
separate_display = TRUE;
|
2019-08-24 08:33:45 +08:00
|
|
|
break;
|
2014-06-06 03:33:40 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
case GIMP_EXPORT_IGNORE:
|
2005-01-04 05:36:43 +08:00
|
|
|
break;
|
2014-06-06 03:33:40 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
case GIMP_EXPORT_CANCEL:
|
|
|
|
return gimp_procedure_new_return_values (procedure,
|
|
|
|
GIMP_PDB_CANCEL,
|
|
|
|
NULL);
|
2005-01-04 05:36:43 +08:00
|
|
|
break;
|
|
|
|
}
|
2019-08-24 08:33:45 +08:00
|
|
|
break;
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2018-01-11 12:21:32 +08:00
|
|
|
|
2020-04-14 17:46:17 +08:00
|
|
|
if (n_drawables != 1)
|
|
|
|
{
|
|
|
|
g_set_error (&error, G_FILE_ERROR, 0,
|
|
|
|
_("JPEG format does not support multiple layers."));
|
|
|
|
|
|
|
|
return gimp_procedure_new_return_values (procedure,
|
|
|
|
GIMP_PDB_CALLING_ERROR,
|
|
|
|
error);
|
|
|
|
}
|
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
/* Override preferences from JPG export defaults (if saved). */
|
|
|
|
|
|
|
|
switch (run_mode)
|
|
|
|
{
|
|
|
|
case GIMP_RUN_NONINTERACTIVE:
|
2020-11-26 05:44:43 +08:00
|
|
|
g_object_set (config, "show-preview", FALSE, NULL);
|
2019-08-24 08:33:45 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case GIMP_RUN_INTERACTIVE:
|
|
|
|
case GIMP_RUN_WITH_LAST_VALS:
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2020-11-26 05:44:43 +08:00
|
|
|
/* restore the values found when loading the file (if available) */
|
|
|
|
gdouble dquality;
|
|
|
|
gint quality;
|
|
|
|
gint subsmp;
|
|
|
|
|
|
|
|
jpeg_restore_original_settings (orig_image,
|
|
|
|
&orig_quality,
|
|
|
|
&orig_subsmp,
|
|
|
|
&orig_num_quant_tables);
|
|
|
|
|
|
|
|
g_object_get (config,
|
|
|
|
"quality", &dquality,
|
|
|
|
"sub-sampling", &subsmp,
|
|
|
|
NULL);
|
|
|
|
|
|
|
|
quality = (gint) (dquality * 100.0);
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
/* If this image was loaded from a JPEG file and has not
|
|
|
|
* been saved yet, try to use some of the settings from the
|
|
|
|
* original file if they are better than the default values.
|
|
|
|
*/
|
2020-11-26 05:44:43 +08:00
|
|
|
if (orig_quality > quality)
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2020-11-26 05:44:43 +08:00
|
|
|
quality = orig_quality;
|
|
|
|
dquality = (gdouble) quality / 100.0;
|
|
|
|
g_object_set (config, "quality", dquality, NULL);
|
2005-01-04 05:36:43 +08:00
|
|
|
}
|
|
|
|
|
2020-11-26 05:44:43 +08:00
|
|
|
if (orig_quality > 0)
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2020-11-26 05:44:43 +08:00
|
|
|
/* Skip changing subsampling to original if we already have
|
|
|
|
* best setting or if original have worst setting
|
|
|
|
*/
|
|
|
|
if (!(subsmp == JPEG_SUBSAMPLING_1x1_1x1_1x1 ||
|
|
|
|
orig_subsmp == JPEG_SUBSAMPLING_2x2_1x1_1x1))
|
|
|
|
{
|
|
|
|
subsmp = orig_subsmp;
|
|
|
|
g_object_set (config, "sub-sampling", orig_subsmp, NULL);
|
|
|
|
}
|
2019-08-24 08:33:45 +08:00
|
|
|
|
2020-11-26 05:44:43 +08:00
|
|
|
if (orig_quality == quality && orig_subsmp == subsmp)
|
|
|
|
{
|
|
|
|
g_object_set (config, "use-original-quality", TRUE, NULL);
|
|
|
|
}
|
2007-07-09 14:42:45 +08:00
|
|
|
}
|
|
|
|
}
|
2019-08-24 08:33:45 +08:00
|
|
|
break;
|
|
|
|
}
|
2020-11-26 05:44:43 +08:00
|
|
|
g_object_set (config,
|
|
|
|
"original-sub-sampling", orig_subsmp,
|
|
|
|
"original-quality", orig_quality,
|
|
|
|
"original-num-quant-tables", orig_num_quant_tables,
|
|
|
|
NULL);
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
if (run_mode == GIMP_RUN_INTERACTIVE)
|
|
|
|
{
|
2020-11-26 05:44:43 +08:00
|
|
|
gboolean show_preview = FALSE;
|
|
|
|
|
|
|
|
g_object_get (config, "show-preview", &show_preview, NULL);
|
|
|
|
if (show_preview)
|
2007-07-09 14:42:45 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
/* we freeze undo saving so that we can avoid sucking up
|
|
|
|
* tile cache with our unneeded preview steps. */
|
|
|
|
gimp_image_undo_freeze (image);
|
2007-07-09 14:42:45 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
undo_touched = TRUE;
|
2005-01-04 05:36:43 +08:00
|
|
|
}
|
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
/* prepare for the preview */
|
|
|
|
preview_image = image;
|
|
|
|
orig_image_global = orig_image;
|
2020-04-14 17:46:17 +08:00
|
|
|
drawable_global = drawables[0];
|
2019-08-24 08:33:45 +08:00
|
|
|
|
|
|
|
/* First acquire information with a dialog */
|
2022-07-19 18:56:32 +08:00
|
|
|
if (! save_dialog (procedure, config, drawables[0], orig_image))
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
status = GIMP_PDB_CANCEL;
|
2005-01-04 05:36:43 +08:00
|
|
|
}
|
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
if (undo_touched)
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
/* thaw undo saving and flush the displays to have them
|
|
|
|
* reflect the current shortcuts
|
2013-11-05 15:49:40 +08:00
|
|
|
*/
|
2019-08-24 08:33:45 +08:00
|
|
|
gimp_image_undo_thaw (image);
|
|
|
|
gimp_displays_flush ();
|
2014-06-06 03:33:40 +08:00
|
|
|
}
|
2019-08-24 08:33:45 +08:00
|
|
|
}
|
2005-01-04 05:36:43 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
if (status == GIMP_PDB_SUCCESS)
|
|
|
|
{
|
2020-11-26 05:44:43 +08:00
|
|
|
if (! save_image (file, config,
|
|
|
|
image, drawables[0], orig_image, FALSE,
|
2019-08-24 08:33:45 +08:00
|
|
|
&error))
|
2005-01-04 05:36:43 +08:00
|
|
|
{
|
2019-08-24 08:33:45 +08:00
|
|
|
status = GIMP_PDB_EXECUTION_ERROR;
|
|
|
|
}
|
|
|
|
}
|
2008-12-28 10:10:32 +08:00
|
|
|
|
2021-01-18 04:53:48 +08:00
|
|
|
if (status == GIMP_PDB_SUCCESS)
|
|
|
|
{
|
|
|
|
if (metadata)
|
|
|
|
gimp_metadata_set_bits_per_sample (metadata, 8);
|
|
|
|
}
|
|
|
|
|
|
|
|
gimp_procedure_config_end_export (config, image, file, status);
|
|
|
|
g_object_unref (config);
|
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
if (export == GIMP_EXPORT_EXPORT)
|
|
|
|
{
|
|
|
|
/* If the image was exported, delete the new display. This also
|
|
|
|
* deletes the image.
|
|
|
|
*/
|
|
|
|
if (display)
|
2021-04-30 09:51:21 +08:00
|
|
|
{
|
|
|
|
gimp_display_delete (display);
|
|
|
|
gimp_display_present (gimp_default_display ());
|
|
|
|
}
|
2019-08-24 08:33:45 +08:00
|
|
|
else
|
2021-04-30 09:51:21 +08:00
|
|
|
{
|
|
|
|
gimp_image_delete (image);
|
|
|
|
}
|
2020-04-14 17:46:17 +08:00
|
|
|
|
|
|
|
g_free (drawables);
|
2019-08-24 08:33:45 +08:00
|
|
|
}
|
2008-12-28 10:10:32 +08:00
|
|
|
|
2019-08-24 08:33:45 +08:00
|
|
|
return gimp_procedure_new_return_values (procedure, status, error);
|
2005-01-04 05:36:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Here's the routine that will replace the standard error_exit method:
|
|
|
|
*/
|
|
|
|
|
|
|
|
void
|
|
|
|
my_error_exit (j_common_ptr cinfo)
|
|
|
|
{
|
|
|
|
/* cinfo->err really points to a my_error_mgr struct, so coerce pointer */
|
|
|
|
my_error_ptr myerr = (my_error_ptr) cinfo->err;
|
|
|
|
|
|
|
|
/* Always display the message. */
|
|
|
|
/* We could postpone this until after returning, if we chose. */
|
|
|
|
(*cinfo->err->output_message) (cinfo);
|
|
|
|
|
|
|
|
/* Return control to the setjmp point */
|
|
|
|
longjmp (myerr->setjmp_buffer, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
my_output_message (j_common_ptr cinfo)
|
|
|
|
{
|
|
|
|
gchar buffer[JMSG_LENGTH_MAX + 1];
|
|
|
|
|
|
|
|
(*cinfo->err->format_message)(cinfo, buffer);
|
2008-11-04 22:05:24 +08:00
|
|
|
|
|
|
|
g_message ("%s", buffer);
|
2005-01-04 05:36:43 +08:00
|
|
|
}
|