morandi 0.12.1 → 0.99.03

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ typedef enum {
2
+ ANGLE_0 = 0,
3
+ ANGLE_90 = 90,
4
+ ANGLE_180 = 180,
5
+ ANGLE_270 = 270
6
+ } rotate_angle_t;
7
+
8
+ static GdkPixbuf *
9
+ pixbuf_rotate(GdkPixbuf *src, rotate_angle_t angle) {
10
+ GdkPixbuf *dest;
11
+ int has_alpha;
12
+ int s_width, s_height, s_rowstride;
13
+ int d_width, d_height, d_rowstride;
14
+ guchar *s_pix;
15
+ guchar *d_pix;
16
+ guchar *sp;
17
+ guchar *dp;
18
+ int i, j, pix_width;
19
+
20
+ if (!src) return NULL;
21
+
22
+ if (angle == ANGLE_0)
23
+ return gdk_pixbuf_copy(src);
24
+
25
+ s_width = gdk_pixbuf_get_width(src);
26
+ s_height = gdk_pixbuf_get_height(src);
27
+ has_alpha = gdk_pixbuf_get_has_alpha(src);
28
+ s_rowstride = gdk_pixbuf_get_rowstride(src);
29
+ s_pix = gdk_pixbuf_get_pixels(src);
30
+
31
+ switch (angle) {
32
+ case ANGLE_90:
33
+ case ANGLE_270:
34
+ d_width = s_height;
35
+ d_height = s_width;
36
+ break;
37
+ default:
38
+ case ANGLE_0:/* Avoid compiler warnings... */
39
+ case ANGLE_180:
40
+ d_width = s_width;
41
+ d_height = s_height;
42
+ break;
43
+ }
44
+
45
+ dest = gdk_pixbuf_new(GDK_COLORSPACE_RGB, has_alpha, 8, d_width, d_height);
46
+ d_rowstride = gdk_pixbuf_get_rowstride(dest);
47
+ d_pix = gdk_pixbuf_get_pixels(dest);
48
+
49
+ pix_width = (has_alpha ? 4 : 3);
50
+
51
+ for (i = 0; i < s_height; i++) {
52
+ sp = s_pix + (i * s_rowstride);
53
+ for (j = 0; j < s_width; j++) {
54
+ switch (angle) {
55
+ case ANGLE_180:
56
+ dp = d_pix + ((d_height - i - 1) * d_rowstride) + ((d_width - j - 1) * pix_width);
57
+ break;
58
+ case ANGLE_90:
59
+ dp = d_pix + (j * d_rowstride) + ((d_width - i - 1) * pix_width);
60
+ break;
61
+ case ANGLE_270:
62
+ dp = d_pix + ((d_height - j - 1) * d_rowstride) + (i * pix_width);
63
+ break;
64
+ default:
65
+ case ANGLE_0:/* Avoid compiler warnings... */
66
+ dp = d_pix + (i * d_rowstride) + (j * pix_width);
67
+ break;
68
+ }
69
+
70
+ *(dp++) = *(sp++); /* red */
71
+ *(dp++) = *(sp++); /* green */
72
+ *(dp++) = *(sp++); /* blue */
73
+ if (has_alpha) *(dp) = *(sp++); /* alpha */
74
+ }
75
+ }
76
+
77
+ return dest;
78
+ }
79
+
@@ -0,0 +1,72 @@
1
+ #define RLUM (0.3086)
2
+ #define GLUM (0.6094)
3
+ #define BLUM (0.0820)
4
+
5
+ // Graphica Obscure
6
+ #define GO_RGB_TO_GREY(r, g, b) ((int)((RLUM * (double)r) + (GLUM * (double)g) + (BLUM * (double)b)))
7
+
8
+ static inline unsigned char pu_clamp(int x) {
9
+ return (x > 255) ? 255 : (x < 0 ? 0 : x);
10
+ }
11
+
12
+ static GdkPixbuf *pixbuf_tint(GdkPixbuf *src, GdkPixbuf *dest, int r, int g, int b, int alpha) {
13
+ int s_has_alpha, d_has_alpha;
14
+ int s_width, s_height, s_rowstride;
15
+ int d_width, d_height, d_rowstride;
16
+ guchar *s_pix, *sp;
17
+ guchar *d_pix, *dp;
18
+ int i, j, pix_width, grey;
19
+
20
+ g_return_val_if_fail(src != NULL, NULL);
21
+ g_return_val_if_fail(dest != NULL, NULL);
22
+
23
+ s_width = gdk_pixbuf_get_width(src);
24
+ s_height = gdk_pixbuf_get_height(src);
25
+ s_has_alpha = gdk_pixbuf_get_has_alpha(src);
26
+ s_rowstride = gdk_pixbuf_get_rowstride(src);
27
+ s_pix = gdk_pixbuf_get_pixels(src);
28
+
29
+ d_width = gdk_pixbuf_get_width(dest);
30
+ d_height = gdk_pixbuf_get_height(dest);
31
+ d_has_alpha = gdk_pixbuf_get_has_alpha(dest);
32
+ d_rowstride = gdk_pixbuf_get_rowstride(dest);
33
+ d_pix = gdk_pixbuf_get_pixels(dest);
34
+
35
+ g_return_val_if_fail(d_width == s_width, NULL);
36
+ g_return_val_if_fail(d_height == s_height, NULL);
37
+ g_return_val_if_fail(d_has_alpha == s_has_alpha, NULL);
38
+
39
+ pix_width = (s_has_alpha ? 4 : 3);
40
+
41
+ for (i = 0; i < s_height; i++) {
42
+ sp = s_pix;
43
+ dp = d_pix;
44
+
45
+ for (j = 0; j < s_width; j++) {
46
+ grey = GO_RGB_TO_GREY(sp[0], sp[1], sp[2]);
47
+
48
+ dp[0] = pu_clamp(pu_clamp(((int) grey + r) * alpha / 255) +
49
+ pu_clamp((int) sp[0] * (255 - alpha) / 255)); /* red */
50
+
51
+ //fprintf(stderr, "alpha=%i, r=%i, grey=%i -> %i + %i = %i\n", alpha, r, grey, pu_clamp(((int)grey + r) * alpha / 255), pu_clamp((int)sp[0] * (255 - alpha) / 255), dp[0]); /* red */
52
+
53
+ dp[1] = pu_clamp(
54
+ pu_clamp((grey + g) * alpha / 255) + pu_clamp((int) sp[1] * (255 - alpha) / 255)); /* green */
55
+ dp[2] = pu_clamp(
56
+ pu_clamp((grey + b) * alpha / 255) + pu_clamp((int) sp[2] * (255 - alpha) / 255)); /* blue */
57
+
58
+ if (s_has_alpha) {
59
+ dp[3] = sp[3]; /* alpha */
60
+ }
61
+
62
+ dp += pix_width;
63
+ sp += pix_width;
64
+ }
65
+
66
+ d_pix += d_rowstride;
67
+ s_pix += s_rowstride;
68
+ }
69
+
70
+ return dest;
71
+ }
72
+
Binary file
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cairo'
4
+ require 'gdk_pixbuf_cairo'
5
+ require 'pango'
6
+
7
+ module Morandi
8
+ # Rounded rectangle function for photo borders
9
+ module CairoExt
10
+ module_function
11
+
12
+ def rounded_rectangle(cr, x1, y1, x2, y2, x_radius = 4, y_radius = nil)
13
+ width = x2 - x1
14
+ height = y2 - y1
15
+ y_radius ||= x_radius
16
+
17
+ x_radius = [x_radius, width / 2].min
18
+ y_radius = [y_radius, height / 2].min
19
+
20
+ xr1 = x_radius
21
+ xr2 = x_radius / 2.0
22
+ yr1 = y_radius
23
+ yr2 = y_radius / 2.0
24
+
25
+ cr.new_path
26
+ cr.move_to(x1 + xr1, y1)
27
+ cr.line_to(x2 - xr1, y1)
28
+ cr.curve_to(x2 - xr2, y1, x2, y1 + yr2, x2, y1 + yr1)
29
+ cr.line_to(x2, y2 - yr1)
30
+ cr.curve_to(x2, y2 - yr2, x2 - xr2, y2, x2 - xr1, y2)
31
+ cr.line_to(x1 + xr1, y2)
32
+ cr.curve_to(x1 + xr2, y2, x1, y2 - yr2, x1, y2 - yr1)
33
+ cr.line_to(x1, y1 + yr1)
34
+ cr.curve_to(x1, y1 + yr2, x1 + xr2, y1, x1 + xr1, y1)
35
+ cr.close_path
36
+ end
37
+ end
38
+ end
39
+
40
+ # Monkey patch Cairo::Context
41
+ module Cairo
42
+ # Add Cairo::Context#set_source_pixbuf without gtk2 depdendency
43
+ class Context
44
+ def set_source_pixbuf(pixbuf, x = 0, y = 0)
45
+ set_source(pixbuf.to_cairo_image_surface, x, y)
46
+ end
47
+ end
48
+
49
+ # Add ImageSurface.to_gdk_pixbuf
50
+ # for converting back to pixbuf without exporting as PNG
51
+ class ImageSurface
52
+ def to_gdk_pixbuf
53
+ GdkPixbufCairo.surface_to_pixbuf(self)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gdk_pixbuf2'
4
+
5
+ module Morandi
6
+ # Utility functions relating to cropping
7
+ module CropUtils
8
+ module_function
9
+
10
+ def autocrop_coords(pixbuf_width, pixbuf_height, target_width, target_height)
11
+ return nil unless target_width
12
+
13
+ aspect = target_width.to_f / target_height
14
+ pixbuf_aspect = pixbuf_width.to_f / pixbuf_height
15
+
16
+ # TODO: this looks wrong - typically relative aspect ratios matter more
17
+ # than whether this is portrait or landscape
18
+ if pixbuf_height > pixbuf_width
19
+ # Portrait image
20
+ # Check whether the aspect ratio is greater or smaller
21
+ # ie. where constraints will hit
22
+ aspect = target_height.to_f / target_width
23
+ end
24
+
25
+ # Landscape
26
+ if aspect > pixbuf_aspect
27
+ # Width constraint - aspect-rect wider
28
+ crop_width = pixbuf_width
29
+ crop_height = (crop_width / aspect).to_i
30
+ else
31
+ # Height constraint - aspect-rect wider
32
+ crop_height = pixbuf_height
33
+ crop_width = (crop_height * aspect).to_i
34
+ end
35
+
36
+ [
37
+ ((pixbuf_width - crop_width) >> 1),
38
+ ((pixbuf_height - crop_height) >> 1),
39
+ crop_width,
40
+ crop_height
41
+ ].map(&:to_i)
42
+ end
43
+
44
+ def apply_crop(pixbuf, x_coord, y_coord, width, height, fill_col = 0xffffffff)
45
+ if x_coord.negative? ||
46
+ y_coord.negative? ||
47
+ ((x_coord + width) > pixbuf.width) ||
48
+ ((y_coord + height) > pixbuf.height)
49
+
50
+ base_pixbuf = GdkPixbuf::Pixbuf.new(
51
+ colorspace: GdkPixbuf::Colorspace::RGB,
52
+ has_alpha: false,
53
+ bits_per_sample: 8,
54
+ width: width,
55
+ height: height
56
+ )
57
+ base_pixbuf.fill!(fill_col)
58
+
59
+ offset_x = [x_coord, 0].max
60
+ offset_y = [y_coord, 0].max
61
+ copy_w = [width, pixbuf.width - offset_x].min
62
+ copy_h = [height, pixbuf.height - offset_y].min
63
+
64
+ paste_x = [x_coord, 0].min * -1
65
+ paste_y = [y_coord, 0].min * -1
66
+
67
+ copy_w = base_pixbuf.width - paste_x if copy_w + paste_x > base_pixbuf.width
68
+ copy_h = base_pixbuf.height - paste_y if copy_h + paste_y > base_pixbuf.height
69
+
70
+ base_pixbuf.composite!(
71
+ pixbuf,
72
+ dest_x: paste_x,
73
+ dest_y: paste_y,
74
+ dest_width: copy_w,
75
+ dest_height: copy_h,
76
+ offset_x: paste_x - offset_x,
77
+ offset_y: paste_y - offset_y,
78
+ scale_x: 1,
79
+ scale_y: 1,
80
+ interpolation_type: :hyper,
81
+ overall_alpha: 255
82
+ )
83
+ pixbuf = base_pixbuf
84
+ else
85
+ x_coord = x_coord.clamp(0, pixbuf.width)
86
+ y_coord = y_coord.clamp(0, pixbuf.height)
87
+ width = width.clamp(1, pixbuf.width - x_coord)
88
+ height = height.clamp(1, pixbuf.height - y_coord)
89
+
90
+ pixbuf = pixbuf.subpixbuf(x_coord, y_coord, width, height)
91
+ end
92
+ pixbuf
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorscore'
4
+
5
+ module Morandi
6
+ # Base Image Op class
7
+ # @!visibility private
8
+ class ImageOperation
9
+ class << self
10
+ def new_from_hash(hash)
11
+ op = allocate
12
+ hash.each_pair do |key, val|
13
+ op.respond_to?(key.intern) && op.instance_variable_set("@#{key}", val)
14
+ end
15
+ op
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def create_pixbuf_from_image_surface(type, width, height)
22
+ surface = Cairo::ImageSurface.new(type, width, height)
23
+ cr = Cairo::Context.new(surface)
24
+
25
+ yield(cr)
26
+
27
+ final_pb = surface.to_gdk_pixbuf
28
+ cr.destroy
29
+ surface.destroy
30
+ final_pb
31
+ end
32
+ end
33
+
34
+ # Straighten operation
35
+ # Does a small (ie. not 90,180,270 deg) rotation and zooms to avoid cropping
36
+ # @!visibility private
37
+ class Straighten < ImageOperation
38
+ attr_accessor :angle
39
+
40
+ def call(_image, pixbuf)
41
+ return pixbuf if angle.zero?
42
+
43
+ rotation_value_rad = angle * (Math::PI / 180)
44
+
45
+ ratio = pixbuf.width.to_f / pixbuf.height
46
+ rh = pixbuf.height / ((ratio * Math.sin(rotation_value_rad.abs)) + Math.cos(rotation_value_rad.abs))
47
+ scale = pixbuf.height / rh.to_f.abs
48
+
49
+ a_ratio = pixbuf.height.to_f / pixbuf.width
50
+ a_rh = pixbuf.width / ((a_ratio * Math.sin(rotation_value_rad.abs)) + Math.cos(rotation_value_rad.abs))
51
+ a_scale = pixbuf.width / a_rh.to_f.abs
52
+
53
+ scale = a_scale if a_scale > scale
54
+
55
+ create_pixbuf_from_image_surface(:rgb24, pixbuf.width, pixbuf.height) do |cr|
56
+ cr.translate(pixbuf.width / 2.0, pixbuf.height / 2.0)
57
+ cr.rotate(rotation_value_rad)
58
+ cr.scale(scale, scale)
59
+ cr.translate(pixbuf.width / -2.0, pixbuf.height / - 2.0)
60
+ cr.set_source_pixbuf(pixbuf)
61
+
62
+ cr.rectangle(0, 0, pixbuf.width, pixbuf.height)
63
+ cr.paint(1.0)
64
+ end
65
+ end
66
+ end
67
+
68
+ # Image Border operation
69
+ # Supports retro (rounded) and square borders
70
+ # Background colour (ie. border colour) can be white, black, dominant (ie. from image)
71
+ # @!visibility private
72
+ class ImageBorder < ImageOperation
73
+ attr_accessor :style, :colour, :crop, :size, :print_size, :shrink, :border_size
74
+
75
+ def call(_image, pixbuf)
76
+ return pixbuf unless %w[square retro].include? @style
77
+
78
+ create_pixbuf_from_image_surface(:rgb24, pixbuf.width, pixbuf.height) do |cr|
79
+ if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
80
+ img_width = size[0]
81
+ img_height = size[1]
82
+ else
83
+ img_width = pixbuf.width
84
+ img_height = pixbuf.height
85
+ end
86
+
87
+ @border_scale = [img_width, img_height].max.to_f / print_size.max.to_i
88
+
89
+ draw_background(cr, img_height, img_width, pixbuf)
90
+
91
+ x = border_width
92
+ y = border_width
93
+
94
+ # This biggest impact will be on the smallest side, so to avoid white
95
+ # edges between photo and border scale by the longest changed side.
96
+ longest_side = [pixbuf.width, pixbuf.height].max.to_f
97
+
98
+ # Should be less than 1
99
+ pb_scale = (longest_side - (border_width * 2)) / longest_side
100
+
101
+ if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
102
+ x -= @crop[0]
103
+ y -= @crop[1]
104
+ end
105
+
106
+ draw_pixbuf(pixbuf, cr, img_height, img_width, pb_scale, x, y)
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ # Width is proportional to output size
113
+ def border_width
114
+ @border_size * @border_scale
115
+ end
116
+
117
+ def draw_pixbuf(pixbuf, cr, img_height, img_width, pb_scale, x, y)
118
+ case style
119
+ when 'retro'
120
+ Morandi::CairoExt.rounded_rectangle(cr, x, y,
121
+ img_width + x - (border_width * 2),
122
+ img_height + y - (border_width * 2), border_width)
123
+ when 'square'
124
+ cr.rectangle(x, y, img_width - (border_width * 2), img_height - (border_width * 2))
125
+ end
126
+ cr.clip
127
+
128
+ if @shrink
129
+ cr.translate(border_width, border_width)
130
+ cr.scale(pb_scale, pb_scale)
131
+ end
132
+ cr.set_source_pixbuf(pixbuf)
133
+ cr.rectangle(0, 0, pixbuf.width, pixbuf.height)
134
+
135
+ cr.paint(1.0)
136
+ end
137
+
138
+ def draw_background(cr, img_height, img_width, pixbuf)
139
+ cr.save do
140
+ cr.translate(-@crop[0], -@crop[1]) if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
141
+
142
+ cr.save do
143
+ cr.set_operator :source
144
+ cr.set_source_rgb 1, 1, 1
145
+ cr.paint
146
+
147
+ cr.rectangle(0, 0, img_width, img_height)
148
+ case colour
149
+ when 'dominant'
150
+ pixbuf.scale_max(400).save(fn = "/tmp/hist-#{$PROCESS_ID}.#{Time.now.to_i}", 'jpeg')
151
+ histogram = Colorscore::Histogram.new(fn)
152
+ FileUtils.rm_f(fn)
153
+ col = histogram.scores.first[1]
154
+ cr.set_source_rgb col.red / 256.0, col.green / 256.0, col.blue / 256.0
155
+ when 'retro'
156
+ cr.set_source_rgb 1, 1, 0.8
157
+ when 'black'
158
+ cr.set_source_rgb 0, 0, 0
159
+ else
160
+ cr.set_source_rgb 1, 1, 1
161
+ end
162
+ cr.fill
163
+ end
164
+ end
165
+ end
166
+ end
167
+
168
+ # Colourify Operation
169
+ # Apply tint to image with variable strength
170
+ # Supports filter, alpha
171
+ class Colourify < ImageOperation
172
+ attr_reader :filter
173
+
174
+ def alpha
175
+ @alpha || 255
176
+ end
177
+
178
+ def sepia(pixbuf)
179
+ MorandiNative::PixbufUtils.tint(pixbuf, 25, 5, -25, alpha)
180
+ end
181
+
182
+ def bluetone(pixbuf)
183
+ MorandiNative::PixbufUtils.tint(pixbuf, -10, 5, 25, alpha)
184
+ end
185
+
186
+ def null(pixbuf)
187
+ pixbuf
188
+ end
189
+ alias full null # WebKiosk
190
+ alias colour null # WebKiosk
191
+
192
+ def greyscale(pixbuf)
193
+ MorandiNative::PixbufUtils.tint(pixbuf, 0, 0, 0, alpha)
194
+ end
195
+ alias bw greyscale # WebKiosk
196
+
197
+ def call(_image, pixbuf)
198
+ if @filter && respond_to?(@filter)
199
+ __send__(@filter, pixbuf)
200
+ else
201
+ pixbuf # Default is nothing
202
+ end
203
+ end
204
+ end
205
+ end