1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /** \file
3  * Inkscape::Extension::Internal::GimpGrad implementation
4  */
5 
6 /*
7  * Authors:
8  *   Ted Gould <ted@gould.cx>
9  *   Abhishek Sharma
10  *
11  * Copyright (C) 2004-2005 Authors
12  *
13  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14  */
15 
16 #include <color-rgba.h>
17 #include "io/sys.h"
18 #include "extension/system.h"
19 #include "svg/css-ostringstream.h"
20 #include "svg/svg-color.h"
21 
22 #include "gimpgrad.h"
23 #include "streq.h"
24 #include "strneq.h"
25 #include "document.h"
26 #include "extension/extension.h"
27 
28 namespace Inkscape {
29 namespace Extension {
30 namespace Internal {
31 
32 /**
33     \brief  A function to allocate anything -- just an example here
34     \param  module  Unused
35     \return Whether the load was successful
36 */
load(Inkscape::Extension::Extension *)37 bool GimpGrad::load (Inkscape::Extension::Extension */*module*/)
38 {
39     // std::cout << "Hey, I'm loading!\n" << std::endl;
40     return TRUE;
41 }
42 
43 /**
44     \brief  A function to remove what was allocated
45     \param  module  Unused
46     \return None
47 */
unload(Inkscape::Extension::Extension *)48 void GimpGrad::unload (Inkscape::Extension::Extension */*module*/)
49 {
50     // std::cout << "Nooo! I'm being unloaded!" << std::endl;
51     return;
52 }
53 
append_css_num(Glib::ustring & str,double const num)54 static void append_css_num(Glib::ustring &str, double const num)
55 {
56     CSSOStringStream stream;
57     stream << num;
58     str += stream.str();
59 }
60 
61 /**
62     \brief  A function to turn a color into a gradient stop
63     \param  in_color  The color for the stop
64     \param  location  Where the stop is placed in the gradient
65     \return The text that is the stop.  Full SVG containing the element.
66 
67     This function encapsulates all of the translation of the ColorRGBA
68     and the location into the gradient.  It is really pretty simple except
69     that the ColorRGBA is in floats that are 0 to 1 and the SVG wants
70     hex values from 0 to 255 for color.  Otherwise mostly this is just
71     turning the values into strings and returning it.
72 */
stop_svg(ColorRGBA const in_color,double const location)73 static Glib::ustring stop_svg(ColorRGBA const in_color, double const location)
74 {
75     Glib::ustring ret("<stop stop-color=\"");
76 
77     char stop_color_css[16];
78     sp_svg_write_color(stop_color_css, sizeof(stop_color_css), in_color.getIntValue());
79     ret += stop_color_css;
80     ret += '"';
81 
82     if (in_color[3] != 1) {
83         ret += " stop-opacity=\"";
84         append_css_num(ret, in_color[3]);
85         ret += '"';
86     }
87     ret += " offset=\"";
88     append_css_num(ret, location);
89     ret += "\"/>\n";
90     return ret;
91 }
92 
93 /**
94     \brief  Actually open the gradient and turn it into an SPDocument
95     \param  module    The input module being used
96     \param  filename  The filename of the gradient to be opened
97     \return A Document with the gradient in it.
98 
99     GIMP gradients are pretty simple (atleast the newer format, this
100     function does not handle the old one yet).  They start out with
101     the like "GIMP Gradient", then name it, and tell how many entries
102     there are.  This function currently ignores the name and the number
103     of entries just reading until it fails.
104 
105     The other small piece of trickery here is that GIMP gradients define
106     a left position, right position and middle position.  SVG gradients
107     have no middle position in them.  In order to handle this case the
108     left and right colors are averaged in a linear manner and the middle
109     position is used for that color.
110 
111     That is another point, the GIMP gradients support many different types
112     of gradients -- linear being the most simple.  This plugin assumes
113     that they are all linear.  Most GIMP gradients are done this way,
114     but it is possible to encounter more complex ones -- which won't be
115     handled correctly.
116 
117     The one optimization that this plugin makes that if the right side
118     of the previous segment is the same color as the left side of the
119     current segment, then the second one is dropped.  This is often
120     done in GIMP gradients and they are not necissary in SVG.
121 
122     What this function does is build up an SVG document with a single
123     linear gradient in it with all the stops of the colors in the GIMP
124     gradient that is passed in.  This document is then turned into a
125     document using the \c sp_document_from_mem.  That is then returned
126     to Inkscape.
127 */
128 SPDocument *
open(Inkscape::Extension::Input *,gchar const * filename)129 GimpGrad::open (Inkscape::Extension::Input */*module*/, gchar const *filename)
130 {
131     Inkscape::IO::dump_fopen_call(filename, "I");
132     FILE *gradient = Inkscape::IO::fopen_utf8name(filename, "r");
133     if (gradient == nullptr) {
134         return nullptr;
135     }
136 
137     {
138         char tempstr[1024];
139         if (fgets(tempstr, 1024, gradient) == nullptr) {
140             // std::cout << "Seems that the read failed" << std::endl;
141             goto error;
142         }
143         if (!streq(tempstr, "GIMP Gradient\n")) {
144             // std::cout << "This doesn't appear to be a GIMP gradient" << std::endl;
145             goto error;
146         }
147 
148         /* Name field. */
149         if (fgets(tempstr, 1024, gradient) == nullptr) {
150             // std::cout << "Seems that the second read failed" << std::endl;
151             goto error;
152         }
153         if (!strneq(tempstr, "Name: ", 6)) {
154             goto error;
155         }
156         /* Handle very long names.  (And also handle nul bytes gracefully: don't use strlen.) */
157         while (memchr(tempstr, '\n', sizeof(tempstr) - 1) == nullptr) {
158             if (fgets(tempstr, sizeof(tempstr), gradient) == nullptr) {
159                 goto error;
160             }
161         }
162 
163         /* n. segments */
164         if (fgets(tempstr, 1024, gradient) == nullptr) {
165             // std::cout << "Seems that the third read failed" << std::endl;
166             goto error;
167         }
168         char *endptr = nullptr;
169         long const n_segs = strtol(tempstr, &endptr, 10);
170         if ((*endptr != '\n')
171             || n_segs < 1) {
172             /* SVG gradients are allowed to have zero stops (treated as `none'), but gimp 2.2
173              * requires at least one segment (i.e. at least two stops) (see gimp_gradient_load in
174              * gimpgradient-load.c).  We try to use the same error handling as gimp, so that
175              * .ggr files that work in one program work in both programs. */
176             goto error;
177         }
178 
179         ColorRGBA prev_color(-1.0, -1.0, -1.0, -1.0);
180         Glib::ustring outsvg("<svg><defs><linearGradient>\n");
181         long n_segs_found = 0;
182         double prev_right = 0.0;
183         while (fgets(tempstr, 1024, gradient) != nullptr) {
184             double dbls[3 + 4 + 4];
185             gchar *p = tempstr;
186             for (double & dbl : dbls) {
187                 gchar *end = nullptr;
188                 double const xi = g_ascii_strtod(p, &end);
189                 if (!end || end == p || !g_ascii_isspace(*end)) {
190                     goto error;
191                 }
192                 if (xi < 0 || 1 < xi) {
193                     goto error;
194                 }
195                 dbl = xi;
196                 p = end + 1;
197             }
198 
199             double const left = dbls[0];
200             if (left != prev_right) {
201                 goto error;
202             }
203             double const middle = dbls[1];
204             if (!(left <= middle)) {
205                 goto error;
206             }
207             double const right = dbls[2];
208             if (!(middle <= right)) {
209                 goto error;
210             }
211 
212             ColorRGBA const leftcolor(dbls[3], dbls[4], dbls[5], dbls[6]);
213             ColorRGBA const rightcolor(dbls[7], dbls[8], dbls[9], dbls[10]);
214             g_assert(11 == G_N_ELEMENTS(dbls));
215 
216             /* Interpolation enums: curve shape and colour space. */
217             {
218                 /* TODO: Currently we silently ignore type & color, assuming linear interpolation in
219                  * sRGB space (or whatever the default in SVG is).  See gimp/app/core/gimpgradient.c
220                  * for how gimp uses these.  We could use gimp functions to sample at a few points, and
221                  * add some intermediate stops to convert to the linear/sRGB interpolation */
222                 int type; /* enum: linear, curved, sine, sphere increasing, sphere decreasing. */
223                 int color_interpolation; /* enum: rgb, hsv anticlockwise, hsv clockwise. */
224                 if (sscanf(p, "%8d %8d", &type, &color_interpolation) != 2) {
225                     continue;
226                 }
227             }
228 
229             if (prev_color != leftcolor) {
230                 outsvg += stop_svg(leftcolor, left);
231             }
232             if (fabs(middle - .5 * (left + right)) > 1e-4) {
233                 outsvg += stop_svg(leftcolor.average(rightcolor), middle);
234             }
235             outsvg += stop_svg(rightcolor, right);
236 
237             prev_color = rightcolor;
238             prev_right = right;
239             ++n_segs_found;
240         }
241         if (prev_right != 1.0) {
242             goto error;
243         }
244 
245         if (n_segs_found != n_segs) {
246             goto error;
247         }
248 
249         outsvg += "</linearGradient></defs></svg>";
250 
251         // std::cout << "SVG Output: " << outsvg << std::endl;
252 
253         fclose(gradient);
254 
255         return SPDocument::createNewDocFromMem(outsvg.c_str(), outsvg.length(), TRUE);
256     }
257 
258 error:
259     fclose(gradient);
260     return nullptr;
261 }
262 
263 #include "clear-n_.h"
264 
init()265 void GimpGrad::init ()
266 {
267     // clang-format off
268     Inkscape::Extension::build_from_mem(
269         "<inkscape-extension xmlns=\"" INKSCAPE_EXTENSION_URI "\">\n"
270             "<name>" N_("GIMP Gradients") "</name>\n"
271             "<id>org.inkscape.input.gimpgrad</id>\n"
272             "<input>\n"
273                 "<extension>.ggr</extension>\n"
274                 "<mimetype>application/x-gimp-gradient</mimetype>\n"
275                 "<filetypename>" N_("GIMP Gradient (*.ggr)") "</filetypename>\n"
276                 "<filetypetooltip>" N_("Gradients used in GIMP") "</filetypetooltip>\n"
277             "</input>\n"
278         "</inkscape-extension>\n", new GimpGrad());
279     // clang-format on
280     return;
281 }
282 
283 } } }  /* namespace Internal; Extension; Inkscape */
284 
285 /*
286   Local Variables:
287   mode:c++
288   c-file-style:"stroustrup"
289   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
290   indent-tabs-mode:nil
291   fill-column:99
292   End:
293 */
294 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
295