source: appstream-generator/src/asgen/image.d @ 4841

Last change on this file since 4841 was 4841, checked in by Juanma, 2 years ago

Initial release

File size: 13.5 KB
Line 
1/*
2 * Copyright (C) 2016 Matthias Klumpp <matthias@tenstral.net>
3 *
4 * Licensed under the GNU Lesser General Public License Version 3
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation, either version 3 of the license, or
9 * (at your option) any later version.
10 *
11 * This software is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
18 */
19
20module asgen.image;
21
22import std.stdio;
23import std.string;
24import std.conv : to;
25import std.path : baseName;
26import std.math;
27import core.stdc.stdarg;
28import core.stdc.stdio;
29
30import asgen.bindings.cairo;
31import asgen.bindings.rsvg;
32import asgen.bindings.gdkpixbuf;
33
34import gi.glibtypes;
35import gi.glib;
36
37import asgen.logging;
38import asgen.config;
39import asgen.font : Font;
40import core.sync.mutex;
41
42private __gshared Mutex fontconfigMutex = null;
43
44
45enum ImageFormat {
46    UNKNOWN,
47    PNG,
48    JPEG,
49    GIF,
50    SVG,
51    SVGZ,
52    XPM
53}
54
55private void optimizePNG (string fname)
56{
57    import std.process;
58
59    auto conf = asgen.config.Config.get ();
60    if (!conf.featureEnabled (GeneratorFeature.OPTIPNG))
61        return;
62
63    // NOTE: Maybe add an option to run optipng with stronger optimization? (>= -o4)
64    auto optipng = execute (["optipng", fname ]);
65    if (optipng.status != 0)
66        logWarning ("Optipng on '%s' failed with error code %s: %s", fname, optipng.status, optipng.output);
67}
68
69/**
70 * Helper method required so we do not modify the Fontconfig
71 * global state while reading it with another process.
72 *
73 * This prevents a weird deadlock when multiple threads are
74 * redering stuff that contains fonts.
75 **/
76private void
77enterFontconfigCriticalSection () @trusted
78{
79    if (fontconfigMutex is null)
80        return;
81    fontconfigMutex.lock ();
82}
83
84/**
85 * Helper method required so we do not modify the Fontconfig
86 * global state while reading it with another process.
87 *
88 * This prevents a weird deadlock when multiple threads are
89 * redering stuff that contains fonts.
90 **/
91private void
92leaveFontconfigCriticalSection () @trusted
93{
94    if (fontconfigMutex is null)
95        return;
96    fontconfigMutex.unlock ();
97}
98
99public void
100setupFontconfigMutex () @trusted
101{
102    fontconfigMutex = new Mutex;
103}
104
105final class Image
106{
107
108private:
109    GdkPixbuf pix;
110
111public:
112
113    private void throwGError (GError *error, string pretext = null)
114    {
115        if (error !is null) {
116            auto msg = fromStringz (error.message).dup;
117            g_error_free (error);
118
119            if (pretext is null)
120                throw new Exception (to!string (msg));
121            else
122                throw new Exception (format ("%s: %s", pretext, to!string (msg)));
123        }
124    }
125
126    this (string fname)
127    {
128        GError *error = null;
129        pix = gdk_pixbuf_new_from_file (fname.toStringz (), &error);
130        throwGError (error, format ("Unable to open image '%s'", baseName (fname)));
131    }
132
133    this (ubyte[] imgBytes, ImageFormat ikind)
134    {
135        import gi.gio;
136        import gio.MemoryInputStream;
137
138        auto istream = new MemoryInputStream ();
139        istream.addData (imgBytes, null);
140
141        GError *error = null;
142        pix = gdk_pixbuf_new_from_stream (cast(GInputStream*) istream.getMemoryInputStreamStruct (), null, &error);
143        throwGError (error, "Failed to load image data");
144    }
145
146    ~this ()
147    {
148        if (pix !is null)
149            g_object_unref (pix);
150    }
151
152    @property
153    uint width ()
154    {
155        return pix.gdk_pixbuf_get_width ();
156    }
157
158    @property
159    uint height ()
160    {
161        return pix.gdk_pixbuf_get_height ();
162    }
163
164    /**
165     * Scale the image to the given size.
166     */
167    void scale (uint newWidth, uint newHeight)
168    {
169        auto resPix = gdk_pixbuf_scale_simple (pix, newWidth, newHeight, GdkInterpType.BILINEAR);
170        if (resPix is null)
171            throw new Exception (format ("Scaling of image to %sx%s failed.", newWidth, newHeight));
172
173        // set our current image to the scaled version
174        g_object_unref (pix);
175        pix = resPix;
176    }
177
178    /**
179     * Scale the image to the given width, preserving
180     * its aspect ratio.
181     */
182    void scaleToWidth (uint newWidth)
183    {
184        import std.math;
185
186        float scaleFactor = cast(float) newWidth / cast (float) width;
187        uint newHeight = to!uint (floor (height * scaleFactor));
188
189        scale (newWidth, newHeight);
190    }
191
192    /**
193     * Scale the image to the given height, preserving
194     * its aspect ratio.
195     */
196    void scaleToHeight (uint newHeight)
197    {
198        import std.math;
199
200        float scaleFactor = cast(float) newHeight / cast(float) height;
201        uint newWidth = to!uint (floor (width * scaleFactor));
202
203        scale (newWidth, newHeight);
204    }
205
206    /**
207     * Scale the image to fir in a square with the given edge length,
208     * and keep its aspect ratio.
209     */
210    void scaleToFit (uint size)
211    {
212        if (height > width) {
213            scaleToHeight (size);
214        } else {
215            scaleToWidth (size);
216        }
217    }
218
219    void savePng (string fname)
220    {
221        GError *error = null;
222        gdk_pixbuf_save (pix, fname.toStringz (), "png", &error, null);
223        throwGError (error);
224
225        optimizePNG (fname);
226    }
227}
228
229final class Canvas
230{
231
232private:
233    cairo_surface_p srf;
234    cairo_p cr;
235
236    int width_;
237    int height_;
238
239public:
240
241    this (int w, int h)
242    {
243         srf = cairo_image_surface_create (cairo_format_t.FORMAT_ARGB32, w, h);
244         cr = cairo_create (srf);
245
246         width_ = w;
247         height_ = h;
248    }
249
250    ~this ()
251    {
252        if (cr !is null)
253            cairo_destroy (cr);
254        if (srf !is null)
255            cairo_surface_destroy (srf);
256    }
257
258    @property
259    uint width ()
260    {
261        return width_;
262        //! return srf.cairo_image_surface_get_width ();
263    }
264
265    @property
266    uint height ()
267    {
268        return height_;
269        //! return srf.cairo_image_surface_get_height ();
270    }
271
272    void renderSvg (ubyte[] svgBytes)
273    {
274        // NOTE: unfortunately, Cairo/RSvg uses Fontconfig internally, so
275        // we need to lock this down since a parallel-processed font
276        // might need to access this too.
277        // This can likely be optimized by checking whether it's really
278        // a Font that is holding the lock (= make only fonts increase the
279        // Mutex counter)
280        enterFontconfigCriticalSection ();
281
282        auto handle = rsvg_handle_new ();
283        scope (exit) {
284            g_object_unref (handle);
285            leaveFontconfigCriticalSection ();
286        }
287
288        auto svgBSize = ubyte.sizeof * svgBytes.length;
289        GError *error = null;
290        rsvg_handle_write (handle, cast(ubyte*) svgBytes, svgBSize, &error);
291        if (error !is null) {
292            auto msg = fromStringz (error.message).dup;
293            g_error_free (error);
294            throw new Exception (to!string (msg));
295        }
296
297        rsvg_handle_close (handle, &error);
298        if (error !is null) {
299            auto msg = fromStringz (error.message).dup;
300            g_error_free (error);
301            throw new Exception (to!string (msg));
302        }
303
304        RsvgDimensionData dims;
305        rsvg_handle_get_dimensions (handle, &dims);
306
307        auto w = cast(double) cairo_image_surface_get_width (srf);
308        auto h = cast(double) cairo_image_surface_get_height (srf);
309
310        // cairo_translate (cr, (w - dims.width) / 2, (h - dims.height) / 2);
311        cairo_scale (cr, w / dims.width, h / dims.height);
312
313        cr.cairo_save ();
314        scope (exit) cr.cairo_restore ();
315        if (!rsvg_handle_render_cairo (handle, cr))
316            throw new Exception ("Rendering of SVG images failed!");
317    }
318
319    /**
320     * Draw a simple line of text without linebreaks to fill the canvas.
321     **/
322    void drawTextLine (const ref Font font, string text, uint borderWidth = 4)
323    {
324        import asgen.bindings.freetype : FT_LOAD_DEFAULT;
325        enterFontconfigCriticalSection ();
326        scope (exit) leaveFontconfigCriticalSection ();
327
328        auto cff = cairo_ft_font_face_create_for_ft_face (font.fontFace, FT_LOAD_DEFAULT);
329        scope (exit) cairo_font_face_destroy (cff);
330
331        // set font face for Cairo surface
332        auto status = cairo_font_face_status (cff);
333        if (status != cairo_status_t.STATUS_SUCCESS)
334            throw new Exception ("Could not set font face for Cairo: %s".format (to!string (status)));
335        cairo_set_font_face (cr, cff);
336
337        cairo_text_extents_t te;
338        uint textSize = 128;
339        while (textSize-- > 0) {
340            cairo_set_font_size (cr, textSize);
341            cairo_text_extents (cr, text.toStringz, &te);
342            if (te.width <= 0.01f || te.height <= 0.01f)
343                continue;
344            if (te.width < this.width - (borderWidth * 2) &&
345                te.height < this.height - (borderWidth * 2))
346                            break;
347            }
348
349        // draw text
350        cairo_move_to (cr,
351                       (this.width / 2) - te.width / 2 - te.x_bearing,
352                       (this.height / 2) - te.height / 2 - te.y_bearing);
353        cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
354        cairo_show_text (cr, text.toStringz);
355
356        cairo_save (cr);
357    }
358
359    /**
360     * Draw a longer text with linebreaks.
361     */
362    void drawText (const ref Font font, string text, const uint borderWidth = 4, const uint linePad = 2)
363    {
364        import asgen.bindings.freetype : FT_LOAD_DEFAULT;
365        enterFontconfigCriticalSection ();
366        scope (exit) leaveFontconfigCriticalSection ();
367
368        auto cff = cairo_ft_font_face_create_for_ft_face (font.fontFace, FT_LOAD_DEFAULT);
369        scope (exit) cairo_font_face_destroy (cff);
370
371        // set font face for Cairo surface
372        auto status = cairo_font_face_status (cff);
373        if (status != cairo_status_t.STATUS_SUCCESS)
374            throw new Exception (format ("Could not set font face for Cairo: %s", to!string (status)));
375        cairo_set_font_face (cr, cff);
376
377        // calculate best font size
378        uint linePadding = linePad;
379        auto lines = text.split ("\n");
380        string longestLine;
381        if (lines.length <= 1) {
382            linePadding = 0;
383            longestLine = text;
384        } else {
385            ulong ll = 0;
386            longestLine = lines[0];
387            foreach (line; lines) {
388                if (line.length > ll)
389                    longestLine = line;
390                    ll = line.length;
391            }
392        }
393
394        cairo_text_extents_t te;
395        uint text_size = 128;
396        while (text_size-- > 0) {
397            cairo_set_font_size (cr, text_size);
398            cairo_text_extents (cr, longestLine.toStringz, &te);
399            if (te.width <= 0.01f || te.height <= 0.01f)
400                continue;
401            if (te.width < this.width - (borderWidth * 2) &&
402                (te.height * lines.length + linePadding) < this.height - (borderWidth * 2))
403                            break;
404            }
405
406        // center text and draw it
407        auto xPos = (this.width / 2) - te.width / 2 - te.x_bearing;
408        auto teHeight = te.height * lines.length + linePadding * (lines.length-1);
409        auto yPos = (teHeight / 2) - teHeight / 2 - te.y_bearing + borderWidth;
410        cairo_move_to (cr, xPos, yPos);
411        cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
412
413        foreach (line; lines) {
414            cairo_show_text (cr, line.toStringz ());
415            yPos += te.height + linePadding;
416            cairo_move_to (cr, xPos, yPos);
417        }
418        cairo_save (cr);
419    }
420
421    void savePng (string fname)
422    {
423        auto status = cairo_surface_write_to_png (srf, fname.toStringz ());
424        if (status != cairo_status_t.STATUS_SUCCESS)
425            throw new Exception (format ("Could not save canvas to PNG: %s", to!string (status)));
426
427        optimizePNG (fname);
428    }
429}
430
431unittest
432{
433    import std.file : getcwd;
434    import std.path : buildPath;
435    import asgen.utils : getTestSamplesDir;
436    writeln ("TEST: ", "Image");
437
438    auto sampleImgPath = buildPath (getTestSamplesDir (), "appstream-logo.png");
439    writeln ("Loading image (file)");
440    auto img = new Image (sampleImgPath);
441
442    writeln ("Scaling image");
443    assert (img.width == 134);
444    assert (img.height == 132);
445    img.scale (64, 64);
446    assert (img.width == 64);
447    assert (img.height == 64);
448
449    writeln ("Storing image");
450    img.savePng ("/tmp/ag-iscale_test.png");
451
452    writeln ("Loading image (data)");
453    ubyte[] data;
454    auto f = File (sampleImgPath, "r");
455    while (!f.eof) {
456        char[300] buf;
457        data ~= f.rawRead (buf);
458    }
459
460    img = new Image (data, ImageFormat.PNG);
461    writeln ("Scaling image (data)");
462    img.scale (124, 124);
463    writeln ("Storing image (data)");
464    img.savePng ("/tmp/ag-iscale-d_test.png");
465
466    writeln ("Rendering SVG");
467    auto sampleSvgPath = buildPath (getTestSamplesDir (), "table.svgz");
468    data = null;
469    f = File (sampleSvgPath, "r");
470    while (!f.eof) {
471        char[300] buf;
472        data ~= f.rawRead (buf);
473    }
474    auto cv = new Canvas (512, 512);
475    cv.renderSvg (data);
476    writeln ("Saving rendered PNG");
477    cv.savePng ("/tmp/ag-svgrender_test1.png");
478
479    writeln ("Font rendering");
480    auto font = new Font (buildPath (getTestSamplesDir (), "NotoSans-Regular.ttf"));
481
482    cv = new Canvas (400, 100);
483    cv.drawText (font,
484                  "Hello World!\nSecond Line!\nThird line - äöüß!\nA very, very, very long line.");
485    cv.savePng ("/tmp/ag-fontrender_test1.png");
486}
Note: See TracBrowser for help on using the repository browser.