diff options
Diffstat (limited to 'src/util/image.vala')
-rw-r--r-- | src/util/image.vala | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/src/util/image.vala b/src/util/image.vala new file mode 100644 index 0000000..e8f93ba --- /dev/null +++ b/src/util/image.vala @@ -0,0 +1,364 @@ +/* Copyright 2009-2014 Yorba Foundation + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +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 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 <u>De</u>rotates 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); +} + |