/* Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU LGPL (version 2.1 or later). * See the COPYING file in this distribution. */ /* XPM */ private const string fallback_image_missing[] = { /* columns rows colors chars-per-pixel */ "48 48 54 1 ", " c #6A6D67", ". c #6C6E69", "X c #72746F", "o c #747672", "O c #777974", "+ c #797B77", "@ c #7C7E7A", "# c #7F817C", "$ c #81837F", "% c #848682", "& c #878984", "* c #888A86", "= c #8C8D8A", "- c #8F908C", "; c #90928E", ": c #949591", "> c #969894", ", c #999B96", "< c #9C9E9A", "1 c #9FA09C", "2 c #A1A39E", "3 c #A4A6A2", "4 c #A6A9A4", "5 c #A9ABA6", "6 c #ACADA9", "7 c #AEB1AB", "8 c #B1B2AF", "9 c #B3B4B1", "0 c #B6B9B3", "q c #B9BCB6", "w c #BDBEBA", "e c #BEC2BB", "r c #C1C4BE", "t c #C5C5C2", "y c #C6C9C3", "u c #C9CCC6", "i c #CCCDCB", "p c #CED2CA", "a c #D2D6CE", "s c #D5D5D3", "d c #D7D8D5", "f c #D9D9D6", "g c #DCDCDA", "h c #DFE0DD", "j c #E0E0DE", "k c #E4E4E2", "l c #E7E8E5", "z c #E9E9E7", "x c #EDEDEC", "c c #EFF0EE", "v c #F1F1EF", "b c #F2F2F1", "n c #FFFFFF", "m c None", /* pixels */ "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmaaaaaaaaaasaisaaaaaaaaaaaaaaaaaaapppiipuuuuumm", "mannnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnym", "manbbbbbbbbbvbbvvvvxvvvbvvvvvvvvccccccxxvxvxxnym", "manbt89898888988888888888999999999999999999txnrm", "manb6======================-;;=;;;;;;;;;;=:7znrm", "manv6&%%**%*%%%%*%%****=======;==;;;;=;=;==7znem", "manv4%%%%$%%%%%%%%%%%=****=======;==-======6znwm", "manx3%%#$$$$##%#$%%%*%**==========-=--=====6znqm", "manx1$@%#$@$###%%%%=****=*===--;;----====*=5lnqm", "manx<@@@@@@@@##%$%%%%****========-==-======5kn0m", "manz<@@@+@+@@@#$%%%%%=%=%===;=:=--------=-*4kn8m", "manz>O+O+O+@@@#$$%%%%========;;=--------=*=3kn9m", "mpnl:OOOOO+@@$##%%%%%=%=====;=;;--;-----===3kn8m", "munl;OooOOO@@@#$%%%%%======;;;;;:;;;;;---==1hn7m", "munl;oXoOO+@###$%%=%=======;::;::::;;---&#+-gn7m", "mynk*XXooOO+$$#$%%%%=%===;;:;:::::;:-$#XooX-fn5m", "mynk*XXOOO@@$$$$%%=%====;::::>::;*#OXXXXXXX*dn5m", "mrnk*oOO@@@#$$$$%=====::::>>>=%@OXOooXXXooo&dn3m", "mrnj*++@$$$$**$===-;;::>>:=@Ooo@XOOoo#o#OOO=fn3m", "menj*@#$$$$$**===;;:::=%@@@@@@O@#O#Oo#o#++@;gn2m", "menj*$$$$&**===;:;=*#@@@@@@@@@@@#######o##%>gn2m", "menh*$$%**===;=*%###########%@@@###$####$%=,gn1m", "m0nj=%***=**&$$$%$$$%$$%$$$$%$$$$$$$$$$%*;:2hn13gn,m", "m0ng****&&&&&&&&&&&&*%*%*%%*%******%**-:,136kz;m", "m0nh***$&&&&&&&&&&&&%**********%*$***;:,1358r7*m", "m8ng*&&&&&&&&&*$&*&&&&$=$=$=&&&&&&**=:,<11>$65mm", "m7ng*&&&&&&&&&**&&&&&&**=$-$&&&&&&*-:,1..&1wi:mm", "m7ng&&&&&&&&=$*$&&&&&***$$$$&&&***=>,1,225;5srw%mmmm", "m4nf3*&&**&&$-$-*******%$;&&*&*->,2365+ui7$mmmmm", "m3nsfffdddsddaffssssdassgaaaassddfgjg4wu;ommmmmm", "m3nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnvq3<$.mmmmmmm", "mm2211111,<,,,,,,>>>>:::;::;----=====@mmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm", "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm" }; bool is_color_parsable(string spec) { Gdk.Color color; return Gdk.Color.parse(spec, out color); } Gdk.RGBA parse_color(string spec) { return fetch_color(spec); } Gdk.RGBA fetch_color(string spec) { Gdk.RGBA rgba = Gdk.RGBA(); if (!rgba.parse(spec)) error("Can't parse color %s", spec); return rgba; } void set_source_color_from_string(Cairo.Context ctx, string spec) { Gdk.RGBA rgba = fetch_color(spec); ctx.set_source_rgba(rgba.red, rgba.green, rgba.blue, rgba.alpha); } private const int MIN_SCALED_WIDTH = 10; private const int MIN_SCALED_HEIGHT = 10; Gdk.Pixbuf get_placeholder_pixbuf () { // Create empty pixbuf. Gdk.Pixbuf? pixbuf = null; try { var icon_theme = Gtk.IconTheme.get_default (); pixbuf = icon_theme.load_icon("image-missing", Gtk.IconSize.DIALOG, 0); } catch (Error error) { pixbuf = new Gdk.Pixbuf.from_xpm_data(fallback_image_missing); warning("Could not load icon from theme: %s", error.message); } return pixbuf; } Gdk.Pixbuf scale_pixbuf(Gdk.Pixbuf pixbuf, int scale, Gdk.InterpType interp, bool scale_up) { Dimensions original = Dimensions.for_pixbuf(pixbuf); Dimensions scaled = original.get_scaled(scale, scale_up); if ((original.width == scaled.width) && (original.height == scaled.height)) return pixbuf; // use sane minimums ... scale_simple will hang if this is too low scaled = scaled.with_min(MIN_SCALED_WIDTH, MIN_SCALED_HEIGHT); return pixbuf.scale_simple(scaled.width, scaled.height, interp); } Gdk.Pixbuf resize_pixbuf(Gdk.Pixbuf pixbuf, Dimensions resized, Gdk.InterpType interp) { Dimensions original = Dimensions.for_pixbuf(pixbuf); if (original.width == resized.width && original.height == resized.height) return pixbuf; // use sane minimums ... scale_simple will hang if this is too low resized = resized.with_min(MIN_SCALED_WIDTH, MIN_SCALED_HEIGHT); return pixbuf.scale_simple(resized.width, resized.height, interp); } private const double DEGREE = Math.PI / 180.0; void draw_rounded_corners_filled(Cairo.Context ctx, Dimensions dim, Gdk.Point origin, double radius_proportion) { context_rounded_corners(ctx, dim, origin, radius_proportion); ctx.paint(); } void context_rounded_corners(Cairo.Context cx, Dimensions dim, Gdk.Point origin, double radius_proportion) { // establish a reasonable range radius_proportion = radius_proportion.clamp(2.0, 100.0); double left = origin.x; double top = origin.y; double right = origin.x + dim.width; double bottom = origin.y + dim.height; // the radius of the corners is proportional to the distance of the minor axis double radius = ((double) dim.minor_axis()) / radius_proportion; // create context and clipping region, starting from the top right arc and working around // clockwise cx.move_to(left, top); cx.arc(right - radius, top + radius, radius, -90 * DEGREE, 0 * DEGREE); cx.arc(right - radius, bottom - radius, radius, 0 * DEGREE, 90 * DEGREE); cx.arc(left + radius, bottom - radius, radius, 90 * DEGREE, 180 * DEGREE); cx.arc(left + radius, top + radius, radius, 180 * DEGREE, 270 * DEGREE); cx.clip(); } inline uchar shift_color_byte(int b, int shift) { return (uchar) (b + shift).clamp(0, 255); } public void shift_colors(Gdk.Pixbuf pixbuf, int red, int green, int blue, int alpha) { assert(red >= -255 && red <= 255); assert(green >= -255 && green <= 255); assert(blue >= -255 && blue <= 255); assert(alpha >= -255 && alpha <= 255); int width = pixbuf.get_width(); int height = pixbuf.get_height(); int rowstride = pixbuf.get_rowstride(); int channels = pixbuf.get_n_channels(); uchar *pixels = pixbuf.get_pixels(); assert(channels >= 3); assert(pixbuf.get_colorspace() == Gdk.Colorspace.RGB); assert(pixbuf.get_bits_per_sample() == 8); for (int y = 0; y < height; y++) { int y_offset = y * rowstride; for (int x = 0; x < width; x++) { int offset = y_offset + (x * channels); if (red != 0) pixels[offset] = shift_color_byte(pixels[offset], red); if (green != 0) pixels[offset + 1] = shift_color_byte(pixels[offset + 1], green); if (blue != 0) pixels[offset + 2] = shift_color_byte(pixels[offset + 2], blue); if (alpha != 0 && channels >= 4) pixels[offset + 3] = shift_color_byte(pixels[offset + 3], alpha); } } } public void dim_pixbuf(Gdk.Pixbuf pixbuf) { PixelTransformer transformer = new PixelTransformer(); SaturationTransformation sat = new SaturationTransformation(SaturationTransformation.MIN_PARAMETER); transformer.attach_transformation(sat); transformer.transform_pixbuf(pixbuf); shift_colors(pixbuf, 0, 0, 0, -100); } bool coord_in_rectangle(int x, int y, Gdk.Rectangle rect) { return (x >= rect.x && x < (rect.x + rect.width) && y >= rect.y && y <= (rect.y + rect.height)); } public bool rectangles_equal(Gdk.Rectangle a, Gdk.Rectangle b) { return (a.x == b.x) && (a.y == b.y) && (a.width == b.width) && (a.height == b.height); } public string rectangle_to_string(Gdk.Rectangle rect) { return "%d,%d %dx%d".printf(rect.x, rect.y, rect.width, rect.height); } public Gdk.Rectangle clamp_rectangle(Gdk.Rectangle original, Dimensions max) { Gdk.Rectangle rect = Gdk.Rectangle(); rect.x = original.x.clamp(0, max.width); rect.y = original.y.clamp(0, max.height); rect.width = original.width.clamp(0, max.width); rect.height = original.height.clamp(0, max.height); return rect; } public Gdk.Point scale_point(Gdk.Point p, double factor) { Gdk.Point result = {0}; result.x = (int) (factor * p.x + 0.5); result.y = (int) (factor * p.y + 0.5); return result; } public Gdk.Point add_points(Gdk.Point p1, Gdk.Point p2) { Gdk.Point result = {0}; result.x = p1.x + p2.x; result.y = p1.y + p2.y; return result; } public Gdk.Point subtract_points(Gdk.Point p1, Gdk.Point p2) { Gdk.Point result = {0}; result.x = p1.x - p2.x; result.y = p1.y - p2.y; return result; } // Converts XRGB/ARGB (Cairo)-formatted pixels to RGBA (GDK). void fix_cairo_pixbuf(Gdk.Pixbuf pixbuf) { uchar *gdk_pixels = pixbuf.pixels; for (int j = 0 ; j < pixbuf.height; ++j) { uchar *p = gdk_pixels; uchar *end = p + 4 * pixbuf.width; while (p < end) { uchar tmp = p[0]; #if G_BYTE_ORDER == G_LITTLE_ENDIAN p[0] = p[2]; p[2] = tmp; #else p[0] = p[1]; p[1] = p[2]; p[2] = p[3]; p[3] = tmp; #endif p += 4; } gdk_pixels += pixbuf.rowstride; } } /** * Finds the size of the smallest axially-aligned rectangle that could contain * a rectangle src_width by src_height, rotated by angle. * * @param src_width The width of the incoming rectangle. * @param src_height The height of the incoming rectangle. * @param angle The amount to rotate by, given in degrees. * @param dest_width The width of the computed rectangle. * @param dest_height The height of the computed rectangle. */ void compute_arb_rotated_size(double src_width, double src_height, double angle, out double dest_width, out double dest_height) { angle = Math.fabs(degrees_to_radians(angle)); assert(angle <= Math.PI_2); dest_width = src_width * Math.cos(angle) + src_height * Math.sin(angle); dest_height = src_height * Math.cos(angle) + src_width * Math.sin(angle); } /** * @brief Rotates a pixbuf to an arbitrary angle, given in degrees, and returns the rotated pixbuf. * * @param source_pixbuf The source image that needs to be angled. * @param angle The angle the source image should be rotated by. */ Gdk.Pixbuf rotate_arb(Gdk.Pixbuf source_pixbuf, double angle) { // if the straightening angle has been reset // or was never set in the first place, nothing // needs to be done to the source image. if (angle == 0.0) { return source_pixbuf; } // Compute how much the corners of the source image will // move by to determine how big the dest pixbuf should be. double x_tmp, y_tmp; compute_arb_rotated_size(source_pixbuf.width, source_pixbuf.height, angle, out x_tmp, out y_tmp); Gdk.Pixbuf dest_pixbuf = new Gdk.Pixbuf( Gdk.Colorspace.RGB, true, 8, (int) Math.round(x_tmp), (int) Math.round(y_tmp)); Cairo.ImageSurface surface = new Cairo.ImageSurface.for_data( (uchar []) dest_pixbuf.pixels, source_pixbuf.has_alpha ? Cairo.Format.ARGB32 : Cairo.Format.RGB24, dest_pixbuf.width, dest_pixbuf.height, dest_pixbuf.rowstride); Cairo.Context context = new Cairo.Context(surface); context.set_source_rgb(0, 0, 0); context.rectangle(0, 0, dest_pixbuf.width, dest_pixbuf.height); context.fill(); context.translate(dest_pixbuf.width / 2, dest_pixbuf.height / 2); context.rotate(degrees_to_radians(angle)); context.translate(- source_pixbuf.width / 2, - source_pixbuf.height / 2); Gdk.cairo_set_source_pixbuf(context, source_pixbuf, 0, 0); context.get_source().set_filter(Cairo.Filter.BEST); context.paint(); // prepare the newly-drawn image for use by // the rest of the pipeline. fix_cairo_pixbuf(dest_pixbuf); return dest_pixbuf; } /** * @brief Rotates a point around the upper left corner of an image to an arbitrary angle, * given in degrees, and returns the rotated point, translated such that it, along with its attendant * image, are in positive x, positive y. * * @note May be subject to slight inaccuracy as Gdk points' coordinates may only be in whole pixels, * so the fractional component is lost. * * @param source_point The point to be rotated and scaled. * @param img_w The width of the source image (unrotated). * @param img_h The height of the source image (unrotated). * @param angle The angle the source image is to be rotated by to straighten it. */ Gdk.Point rotate_point_arb(Gdk.Point source_point, int img_w, int img_h, double angle, bool invert = false) { // angle of 0 degrees or angle was never set? if (angle == 0.0) { // nothing needs to be done. return source_point; } double dest_width; double dest_height; compute_arb_rotated_size(img_w, img_h, angle, out dest_width, out dest_height); Cairo.Matrix matrix = Cairo.Matrix.identity(); matrix.translate(dest_width / 2, dest_height / 2); matrix.rotate(degrees_to_radians(angle)); matrix.translate(- img_w / 2, - img_h / 2); if (invert) assert(matrix.invert() == Cairo.Status.SUCCESS); double dest_x = source_point.x; double dest_y = source_point.y; matrix.transform_point(ref dest_x, ref dest_y); return { (int) dest_x, (int) dest_y }; } /** * @brief Derotates a point around the upper left corner of an image from an arbitrary angle, * given in degrees, and returns the de-rotated point, taking into account any translation necessary * to make sure all of the rotated image stays in positive x, positive y. * * @note May be subject to slight inaccuracy as Gdk points' coordinates may only be in whole pixels, * so the fractional component is lost. * * @param source_point The point to be de-rotated. * @param img_w The width of the source image (unrotated). * @param img_h The height of the source image (unrotated). * @param angle The angle the source image is to be rotated by to straighten it. */ Gdk.Point derotate_point_arb(Gdk.Point source_point, int img_w, int img_h, double angle) { return rotate_point_arb(source_point, img_w, img_h, angle, true); } // Force an axially-aligned box to be inside a rotated rectangle. Box clamp_inside_rotated_image(Box src, int img_w, int img_h, double angle_deg, bool preserve_geom) { Gdk.Point top_left = derotate_point_arb({src.left, src.top}, img_w, img_h, angle_deg); Gdk.Point top_right = derotate_point_arb({src.right, src.top}, img_w, img_h, angle_deg); Gdk.Point bottom_left = derotate_point_arb({src.left, src.bottom}, img_w, img_h, angle_deg); Gdk.Point bottom_right = derotate_point_arb({src.right, src.bottom}, img_w, img_h, angle_deg); double angle = degrees_to_radians(angle_deg); int top_offset = 0, bottom_offset = 0, left_offset = 0, right_offset = 0; int top = int.min(top_left.y, top_right.y); if (top < 0) top_offset = (int) ((0 - top) * Math.cos(angle)); int bottom = int.max(bottom_left.y, bottom_right.y); if (bottom > img_h) bottom_offset = (int) ((img_h - bottom) * Math.cos(angle)); int left = int.min(top_left.x, bottom_left.x); if (left < 0) left_offset = (int) ((0 - left) * Math.cos(angle)); int right = int.max(top_right.x, bottom_right.x); if (right > img_w) right_offset = (int) ((img_w - right) * Math.cos(angle)); return preserve_geom ? src.get_offset(left_offset + right_offset, top_offset + bottom_offset) : Box(src.left + left_offset, src.top + top_offset, src.right + right_offset, src.bottom + bottom_offset); }