morandi 0.12.1 → 0.99.03

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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