morandi 0.12.1 → 0.99.4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -2
- data/README.md +9 -21
- data/ext/gdk_pixbuf_cairo/extconf.rb +124 -0
- data/ext/gdk_pixbuf_cairo/gdk_pixbuf_cairo.c +260 -0
- data/ext/morandi_native/extconf.rb +121 -0
- data/ext/morandi_native/filter.h +229 -0
- data/ext/morandi_native/gamma.h +48 -0
- data/ext/morandi_native/mask.h +75 -0
- data/ext/morandi_native/morandi_native.c +1126 -0
- data/ext/morandi_native/rotate.h +79 -0
- data/ext/morandi_native/tint.h +72 -0
- data/lib/gdk_pixbuf_cairo.so +0 -0
- data/lib/morandi/cairo_ext.rb +56 -0
- data/lib/morandi/crop_utils.rb +95 -0
- data/lib/morandi/errors.rb +12 -0
- data/lib/morandi/image_operation.rb +31 -0
- data/lib/morandi/image_processor.rb +186 -180
- data/lib/morandi/operation/colourify.rb +45 -0
- data/lib/morandi/operation/image_border.rb +108 -0
- data/lib/morandi/operation/straighten.rb +41 -0
- data/lib/morandi/pixbuf_ext.rb +20 -0
- data/lib/morandi/profiled_pixbuf.rb +18 -71
- data/lib/morandi/redeye.rb +35 -47
- data/lib/morandi/srgb_conversion.rb +42 -0
- data/lib/morandi/version.rb +3 -1
- data/lib/morandi.rb +42 -12
- data/lib/morandi_native.so +0 -0
- metadata +57 -116
- data/.gitignore +0 -18
- data/.rspec +0 -3
- data/.ruby-version +0 -1
- data/Gemfile +0 -4
- data/Rakefile +0 -1
- data/lib/morandi/image-ops.rb +0 -307
- data/lib/morandi/utils.rb +0 -136
- data/morandi.gemspec +0 -33
- data/sample/sample.jpg +0 -0
- data/spec/morandi_spec.rb +0 -208
- data/spec/spec_helper.rb +0 -19
@@ -1,245 +1,251 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'morandi/profiled_pixbuf'
|
2
4
|
require 'morandi/redeye'
|
5
|
+
require 'morandi/operation/straighten'
|
6
|
+
require 'morandi/operation/colourify'
|
7
|
+
require 'morandi/operation/image_border'
|
3
8
|
|
4
|
-
|
5
|
-
|
6
|
-
attr_accessor :config
|
7
|
-
|
8
|
-
def self.default_icc_path(path)
|
9
|
-
"#{path}.icc.jpg"
|
10
|
-
end
|
9
|
+
module Morandi
|
10
|
+
# rubocop:disable Metrics/ClassLength
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
# ImageProcessor transforms an image.
|
13
|
+
class ImageProcessor
|
14
|
+
attr_reader :options, :pb
|
14
15
|
|
15
|
-
|
16
|
+
def initialize(file, user_options, local_options = {})
|
17
|
+
@file = file
|
16
18
|
|
17
|
-
|
18
|
-
@options = (local_options || {}).merge(user_options || {})
|
19
|
-
@local_options = local_options
|
19
|
+
user_options.keys.grep(/^path/).each { |k| user_options.delete(k) }
|
20
20
|
|
21
|
-
|
22
|
-
|
21
|
+
# Give priority to user_options
|
22
|
+
@options = (local_options || {}).merge(user_options || {})
|
23
|
+
@local_options = local_options
|
23
24
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@pb = @file
|
28
|
-
@scale = 1.0
|
25
|
+
@max_size_px = @options['output.max']
|
26
|
+
@width = @options['output.width']
|
27
|
+
@height = @options['output.height']
|
29
28
|
end
|
30
|
-
end
|
31
29
|
|
32
|
-
|
33
|
-
|
34
|
-
|
30
|
+
def process!
|
31
|
+
case @file
|
32
|
+
when String
|
33
|
+
get_pixbuf
|
34
|
+
when GdkPixbuf::Pixbuf, Morandi::ProfiledPixbuf
|
35
|
+
@pb = @file
|
36
|
+
@scale = 1.0
|
37
|
+
end
|
38
|
+
|
39
|
+
# Apply Red-Eye corrections
|
40
|
+
apply_redeye!
|
35
41
|
|
36
|
-
|
37
|
-
|
42
|
+
# Apply contrast, brightness etc
|
43
|
+
apply_colour_manipulations!
|
38
44
|
|
39
|
-
|
40
|
-
|
45
|
+
# apply rotation
|
46
|
+
apply_rotate!
|
41
47
|
|
42
|
-
|
43
|
-
|
48
|
+
# apply crop
|
49
|
+
apply_crop!
|
44
50
|
|
45
|
-
|
46
|
-
|
51
|
+
# apply filter
|
52
|
+
apply_filters!
|
47
53
|
|
48
|
-
|
49
|
-
|
54
|
+
# add border
|
55
|
+
apply_decorations!
|
50
56
|
|
51
|
-
|
52
|
-
|
57
|
+
@pb = @pb.scale_max([@width, @height].max) if @options['output.limit'] && @width && @height
|
58
|
+
|
59
|
+
@pb
|
60
|
+
rescue GdkPixbuf::PixbufError::UnknownType => e
|
61
|
+
raise UnknownTypeError, e.message
|
62
|
+
rescue GdkPixbuf::PixbufError::CorruptImage => e
|
63
|
+
raise CorruptImageError, e.message
|
53
64
|
end
|
54
65
|
|
55
|
-
|
56
|
-
|
66
|
+
# Returns generated pixbuf
|
67
|
+
def result
|
68
|
+
process! unless @pb
|
69
|
+
@pb
|
70
|
+
end
|
57
71
|
|
58
|
-
|
59
|
-
|
60
|
-
end
|
72
|
+
def write_to_png(write_to, orientation = :any)
|
73
|
+
pb = @pb
|
61
74
|
|
62
|
-
|
63
|
-
|
75
|
+
case orientation
|
76
|
+
when :landscape
|
77
|
+
pb = @pb.rotate(90) if @pb.width < @pb.height
|
78
|
+
when :portrait
|
79
|
+
pb = @pb.rotate(90) if @pb.width > @pb.height
|
80
|
+
end
|
81
|
+
pb.save(write_to, 'png')
|
82
|
+
end
|
64
83
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
when :portrait
|
69
|
-
pb = @pb.rotate(90) if @pb.width > @pb.height
|
84
|
+
def write_to_jpeg(write_to, quality = nil)
|
85
|
+
quality ||= options.fetch('quality', '97')
|
86
|
+
@pb.save(write_to, 'jpeg', quality: quality.to_s)
|
70
87
|
end
|
71
|
-
pb.save(fn, 'png')
|
72
|
-
end
|
73
88
|
|
74
|
-
|
75
|
-
quality ||= options.fetch('quality', '97')
|
76
|
-
@pb.save(fn, 'jpeg', quality: quality.to_s)
|
77
|
-
end
|
89
|
+
protected
|
78
90
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
if @scale_to
|
84
|
-
@pb = Morandi::ProfiledPixbuf.new(@file, @scale_to, @scale_to, @local_options)
|
85
|
-
@src_max = [width, height].max
|
86
|
-
@actual_max = [@pb.width, @pb.height].max
|
87
|
-
else
|
88
|
-
@pb = Morandi::ProfiledPixbuf.new(@file, @local_options)
|
89
|
-
@src_max = [@pb.width, @pb.height].max
|
90
|
-
@actual_max = [@pb.width, @pb.height].max
|
91
|
-
end
|
91
|
+
def get_pixbuf
|
92
|
+
_, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
|
93
|
+
@pb = Morandi::ProfiledPixbuf.new(@file, @local_options, @max_size_px)
|
92
94
|
|
93
|
-
|
94
|
-
|
95
|
+
# Everything below probably could be substituted with the following:
|
96
|
+
# @scale = @max_size_px ? @max_size_px / [width, height].max : 1.0
|
97
|
+
actual_max = [@pb.width, @pb.height].max
|
98
|
+
src_max = if @max_size_px
|
99
|
+
[width, height].max
|
100
|
+
else
|
101
|
+
[@pb.width, @pb.height].max
|
102
|
+
end
|
95
103
|
|
96
|
-
|
97
|
-
-1, -1, -1, -1, -1,
|
98
|
-
-1, 2, 2, 2, -1,
|
99
|
-
-1, 2, 8, 2, -1,
|
100
|
-
-1, 2, 2, 2, -1,
|
101
|
-
-1, -1, -1, -1, -1,
|
102
|
-
]
|
103
|
-
BLUR = [
|
104
|
-
0, 1, 1, 1, 0,
|
105
|
-
1, 1, 1, 1, 1,
|
106
|
-
1, 1, 1, 1, 1,
|
107
|
-
1, 1, 1, 1, 1,
|
108
|
-
0, 1, 1, 1, 0,
|
109
|
-
]
|
110
|
-
|
111
|
-
def apply_colour_manipulations!
|
112
|
-
if options['brighten'].to_i.nonzero?
|
113
|
-
brighten = [ [ 5 * options['brighten'], -100 ].max, 100 ].min
|
114
|
-
@pb = PixbufUtils.brightness(@pb, brighten)
|
104
|
+
@scale = actual_max / src_max.to_f
|
115
105
|
end
|
116
106
|
|
117
|
-
|
118
|
-
|
119
|
-
|
107
|
+
SHARPEN = [
|
108
|
+
-1, -1, -1, -1, -1,
|
109
|
+
-1, 2, 2, 2, -1,
|
110
|
+
-1, 2, 8, 2, -1,
|
111
|
+
-1, 2, 2, 2, -1,
|
112
|
+
-1, -1, -1, -1, -1
|
113
|
+
].freeze
|
120
114
|
|
121
|
-
|
122
|
-
|
123
|
-
|
115
|
+
BLUR = [
|
116
|
+
0, 1, 1, 1, 0,
|
117
|
+
1, 1, 1, 1, 1,
|
118
|
+
1, 1, 1, 1, 1,
|
119
|
+
1, 1, 1, 1, 1,
|
120
|
+
0, 1, 1, 1, 0
|
121
|
+
].freeze
|
122
|
+
|
123
|
+
def apply_colour_manipulations!
|
124
|
+
if options['brighten'].to_i.nonzero?
|
125
|
+
brighten = (5 * options['brighten']).clamp(-100, 100)
|
126
|
+
@pb = MorandiNative::PixbufUtils.brightness(@pb, brighten)
|
127
|
+
end
|
128
|
+
|
129
|
+
if options['gamma'] && not_equal_to_one(options['gamma'])
|
130
|
+
@pb = MorandiNative::PixbufUtils.gamma(@pb,
|
131
|
+
options['gamma'])
|
132
|
+
end
|
133
|
+
|
134
|
+
if options['contrast'].to_i.nonzero?
|
135
|
+
contrast = (5 * options['contrast']).clamp(-100, 100)
|
136
|
+
@pb = MorandiNative::PixbufUtils.contrast(@pb, contrast)
|
137
|
+
end
|
138
|
+
|
139
|
+
return unless options['sharpen'].to_i.nonzero?
|
124
140
|
|
125
|
-
|
126
|
-
if options['sharpen'] > 0
|
141
|
+
if options['sharpen'].positive?
|
127
142
|
[options['sharpen'], 5].min.times do
|
128
|
-
@pb = PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
|
143
|
+
@pb = MorandiNative::PixbufUtils.filter(@pb, SHARPEN, SHARPEN.inject(0, &:+))
|
129
144
|
end
|
130
|
-
elsif options['sharpen']
|
131
|
-
[
|
132
|
-
@pb = PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
|
145
|
+
elsif options['sharpen'].negative?
|
146
|
+
[(options['sharpen'] * -1), 5].min.times do
|
147
|
+
@pb = MorandiNative::PixbufUtils.filter(@pb, BLUR, BLUR.inject(0, &:+))
|
133
148
|
end
|
134
149
|
end
|
135
150
|
end
|
136
|
-
end
|
137
151
|
|
138
|
-
|
139
|
-
|
140
|
-
|
152
|
+
def apply_redeye!
|
153
|
+
(options['redeye'] || []).each do |eye|
|
154
|
+
@pb = Morandi::RedEye::TapRedEye.tap_on(@pb, eye[0] * @scale, eye[1] * @scale)
|
155
|
+
end
|
141
156
|
end
|
142
|
-
end
|
143
157
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
(360-a)%360
|
148
|
-
else
|
149
|
-
nil
|
158
|
+
def angle
|
159
|
+
a = options['angle'].to_i
|
160
|
+
(360 - a) % 360 if a
|
150
161
|
end
|
151
|
-
end
|
152
162
|
|
153
|
-
|
154
|
-
|
155
|
-
|
163
|
+
# modifies @pb with any applied rotation
|
164
|
+
def apply_rotate!
|
165
|
+
a = angle
|
156
166
|
|
157
|
-
|
158
|
-
|
167
|
+
@pb = @pb.rotate(a) unless (a % 360).zero?
|
168
|
+
|
169
|
+
unless options['straighten'].to_f.zero?
|
170
|
+
@pb = Morandi::Operation::Straighten.new_from_hash(angle: options['straighten'].to_f).call(@pb)
|
171
|
+
end
|
172
|
+
|
173
|
+
@image_width = @pb.width
|
174
|
+
@image_height = @pb.height
|
159
175
|
end
|
160
176
|
|
161
|
-
|
162
|
-
|
177
|
+
DEFAULT_CONFIG = {
|
178
|
+
'border-size-mm' => 5
|
179
|
+
}.freeze
|
180
|
+
def config_for(key)
|
181
|
+
return options[key] if options&.key?(key)
|
182
|
+
|
183
|
+
DEFAULT_CONFIG[key]
|
163
184
|
end
|
164
185
|
|
165
|
-
|
166
|
-
|
167
|
-
end
|
186
|
+
def apply_crop!
|
187
|
+
crop = options['crop']
|
168
188
|
|
169
|
-
|
170
|
-
'border-size-mm' => 5
|
171
|
-
}
|
172
|
-
def config_for(key)
|
173
|
-
return options[key] if options && options.has_key?(key)
|
174
|
-
return @config[key] if @config && @config.has_key?(key)
|
175
|
-
DEFAULT_CONFIG[key]
|
176
|
-
end
|
189
|
+
return if crop.nil? && config_for('image.auto-crop').eql?(false)
|
177
190
|
|
178
|
-
|
179
|
-
def apply_crop!
|
180
|
-
crop = options['crop']
|
191
|
+
crop = crop.split(',').map(&:to_i) if crop.is_a?(String) && crop =~ /^\d+,\d+,\d+,\d+/
|
181
192
|
|
182
|
-
|
183
|
-
|
184
|
-
|
193
|
+
crop = nil unless crop.is_a?(Array) && crop.size.eql?(4) && crop.all? do |i|
|
194
|
+
i.is_a?(Numeric)
|
195
|
+
end
|
185
196
|
|
186
|
-
|
187
|
-
|
188
|
-
end
|
197
|
+
# can't crop, won't crop
|
198
|
+
return if @width.nil? && @height.nil? && crop.nil?
|
189
199
|
|
190
|
-
|
191
|
-
i.kind_of?(Numeric)
|
192
|
-
}
|
200
|
+
crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
|
193
201
|
|
194
|
-
|
195
|
-
return if @width.nil? && @height.nil? && crop.nil?
|
202
|
+
crop ||= Morandi::CropUtils.autocrop_coords(@pb.width, @pb.height, @width, @height)
|
196
203
|
|
197
|
-
|
198
|
-
crop = crop.map { |s| (s.to_f * @scale).floor }
|
204
|
+
@pb = Morandi::CropUtils.apply_crop(@pb, crop[0], crop[1], crop[2], crop[3])
|
199
205
|
end
|
200
206
|
|
201
|
-
|
207
|
+
def apply_filters!
|
208
|
+
filter = options['fx']
|
202
209
|
|
203
|
-
|
204
|
-
|
210
|
+
case filter
|
211
|
+
when 'greyscale', 'sepia', 'bluetone'
|
212
|
+
op = Morandi::Operation::Colourify.new_from_hash('filter' => filter)
|
213
|
+
else
|
214
|
+
return
|
215
|
+
end
|
216
|
+
@pb = op.call(@pb)
|
217
|
+
end
|
205
218
|
|
219
|
+
def apply_decorations!
|
220
|
+
style = options['border-style']
|
221
|
+
colour = options['background-style']
|
206
222
|
|
207
|
-
|
208
|
-
|
223
|
+
return if style.nil? || style.eql?('none')
|
224
|
+
return if colour.eql?('none')
|
209
225
|
|
210
|
-
|
211
|
-
when 'greyscale'
|
212
|
-
op = Morandi::Colourify.new_from_hash('op' => filter)
|
213
|
-
when 'sepia', 'bluetone'
|
214
|
-
# could also set 'alpha' => (0.85 * 255).to_i
|
215
|
-
op = Morandi::Colourify.new_from_hash('op' => filter)
|
216
|
-
else
|
217
|
-
return
|
218
|
-
end
|
219
|
-
@pb = op.call(nil, @pb)
|
220
|
-
end
|
226
|
+
colour ||= 'black'
|
221
227
|
|
222
|
-
|
223
|
-
|
228
|
+
crop = options['crop']
|
229
|
+
crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
|
224
230
|
|
225
|
-
|
226
|
-
|
227
|
-
|
231
|
+
op = Morandi::Operation::ImageBorder.new_from_hash(
|
232
|
+
'style' => style,
|
233
|
+
'colour' => colour || '#000000',
|
234
|
+
'crop' => crop,
|
235
|
+
'size' => [@image_width, @image_height],
|
236
|
+
'print_size' => [@width, @height],
|
237
|
+
'shrink' => true,
|
238
|
+
'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
|
239
|
+
)
|
228
240
|
|
229
|
-
|
230
|
-
|
241
|
+
@pb = op.call(@pb)
|
242
|
+
end
|
231
243
|
|
232
|
-
|
233
|
-
'style' => style,
|
234
|
-
'colour' => colour || '#000000',
|
235
|
-
'crop' => crop,
|
236
|
-
'size' => [@image_width, @image_height],
|
237
|
-
'print_size' => [@width, @height],
|
238
|
-
'shrink' => true,
|
239
|
-
'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
|
240
|
-
})
|
244
|
+
private
|
241
245
|
|
242
|
-
|
246
|
+
def not_equal_to_one(float)
|
247
|
+
(float - 1.0).abs >= Float::EPSILON
|
248
|
+
end
|
243
249
|
end
|
244
|
-
|
250
|
+
# rubocop:enable Metrics/ClassLength
|
245
251
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'morandi/image_operation'
|
4
|
+
|
5
|
+
module Morandi
|
6
|
+
module Operation
|
7
|
+
# Colourify Operation
|
8
|
+
# Apply tint to image with variable strength
|
9
|
+
# Supports filter, alpha
|
10
|
+
class Colourify < ImageOperation
|
11
|
+
attr_reader :filter
|
12
|
+
|
13
|
+
def alpha
|
14
|
+
@alpha || 255
|
15
|
+
end
|
16
|
+
|
17
|
+
def sepia(pixbuf)
|
18
|
+
MorandiNative::PixbufUtils.tint(pixbuf, 25, 5, -25, alpha)
|
19
|
+
end
|
20
|
+
|
21
|
+
def bluetone(pixbuf)
|
22
|
+
MorandiNative::PixbufUtils.tint(pixbuf, -10, 5, 25, alpha)
|
23
|
+
end
|
24
|
+
|
25
|
+
def null(pixbuf)
|
26
|
+
pixbuf
|
27
|
+
end
|
28
|
+
alias full null # WebKiosk
|
29
|
+
alias colour null # WebKiosk
|
30
|
+
|
31
|
+
def greyscale(pixbuf)
|
32
|
+
MorandiNative::PixbufUtils.tint(pixbuf, 0, 0, 0, alpha)
|
33
|
+
end
|
34
|
+
alias bw greyscale # WebKiosk
|
35
|
+
|
36
|
+
def call(pixbuf)
|
37
|
+
if @filter && respond_to?(@filter)
|
38
|
+
__send__(@filter, pixbuf)
|
39
|
+
else
|
40
|
+
pixbuf # Default is nothing
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'colorscore'
|
4
|
+
require 'morandi/image_operation'
|
5
|
+
|
6
|
+
module Morandi
|
7
|
+
module Operation
|
8
|
+
# Image Border operation
|
9
|
+
# Supports retro (rounded) and square borders
|
10
|
+
# Background colour (ie. border colour) can be white, black, dominant (ie. from image)
|
11
|
+
# @!visibility private
|
12
|
+
class ImageBorder < ImageOperation
|
13
|
+
attr_accessor :style, :colour, :crop, :size, :print_size, :shrink, :border_size
|
14
|
+
|
15
|
+
def call(pixbuf)
|
16
|
+
return pixbuf unless %w[square retro].include? @style
|
17
|
+
|
18
|
+
create_pixbuf_from_image_surface(:rgb24, pixbuf.width, pixbuf.height) do |cr|
|
19
|
+
if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
|
20
|
+
img_width = size[0]
|
21
|
+
img_height = size[1]
|
22
|
+
else
|
23
|
+
img_width = pixbuf.width
|
24
|
+
img_height = pixbuf.height
|
25
|
+
end
|
26
|
+
|
27
|
+
@border_scale = [img_width, img_height].max.to_f / print_size.max.to_i
|
28
|
+
|
29
|
+
draw_background(cr, img_height, img_width, pixbuf)
|
30
|
+
|
31
|
+
x = border_width
|
32
|
+
y = border_width
|
33
|
+
|
34
|
+
# This biggest impact will be on the smallest side, so to avoid white
|
35
|
+
# edges between photo and border scale by the longest changed side.
|
36
|
+
longest_side = [pixbuf.width, pixbuf.height].max.to_f
|
37
|
+
|
38
|
+
# Should be less than 1
|
39
|
+
pb_scale = (longest_side - (border_width * 2)) / longest_side
|
40
|
+
|
41
|
+
if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
|
42
|
+
x -= @crop[0]
|
43
|
+
y -= @crop[1]
|
44
|
+
end
|
45
|
+
|
46
|
+
draw_pixbuf(pixbuf, cr, img_height, img_width, pb_scale, x, y)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Width is proportional to output size
|
53
|
+
def border_width
|
54
|
+
@border_size * @border_scale
|
55
|
+
end
|
56
|
+
|
57
|
+
def draw_pixbuf(pixbuf, cr, img_height, img_width, pb_scale, x, y)
|
58
|
+
case style
|
59
|
+
when 'retro'
|
60
|
+
Morandi::CairoExt.rounded_rectangle(cr, x, y,
|
61
|
+
img_width + x - (border_width * 2),
|
62
|
+
img_height + y - (border_width * 2), border_width)
|
63
|
+
when 'square'
|
64
|
+
cr.rectangle(x, y, img_width - (border_width * 2), img_height - (border_width * 2))
|
65
|
+
end
|
66
|
+
cr.clip
|
67
|
+
|
68
|
+
if @shrink
|
69
|
+
cr.translate(border_width, border_width)
|
70
|
+
cr.scale(pb_scale, pb_scale)
|
71
|
+
end
|
72
|
+
cr.set_source_pixbuf(pixbuf)
|
73
|
+
cr.rectangle(0, 0, pixbuf.width, pixbuf.height)
|
74
|
+
|
75
|
+
cr.paint(1.0)
|
76
|
+
end
|
77
|
+
|
78
|
+
def draw_background(cr, img_height, img_width, pixbuf)
|
79
|
+
cr.save do
|
80
|
+
cr.translate(-@crop[0], -@crop[1]) if @crop && ((@crop[0]).negative? || (@crop[1]).negative?)
|
81
|
+
|
82
|
+
cr.save do
|
83
|
+
cr.set_operator :source
|
84
|
+
cr.set_source_rgb 1, 1, 1
|
85
|
+
cr.paint
|
86
|
+
|
87
|
+
cr.rectangle(0, 0, img_width, img_height)
|
88
|
+
case colour
|
89
|
+
when 'dominant'
|
90
|
+
pixbuf.scale_max(400).save(fn = "/tmp/hist-#{$PROCESS_ID}.#{Time.now.to_i}", 'jpeg')
|
91
|
+
histogram = Colorscore::Histogram.new(fn)
|
92
|
+
FileUtils.rm_f(fn)
|
93
|
+
col = histogram.scores.first[1]
|
94
|
+
cr.set_source_rgb col.red / 256.0, col.green / 256.0, col.blue / 256.0
|
95
|
+
when 'retro'
|
96
|
+
cr.set_source_rgb 1, 1, 0.8
|
97
|
+
when 'black'
|
98
|
+
cr.set_source_rgb 0, 0, 0
|
99
|
+
else
|
100
|
+
cr.set_source_rgb 1, 1, 1
|
101
|
+
end
|
102
|
+
cr.fill
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'morandi/image_operation'
|
4
|
+
|
5
|
+
module Morandi
|
6
|
+
module Operation
|
7
|
+
# Straighten operation
|
8
|
+
# Does a small (ie. not 90,180,270 deg) rotation and zooms to avoid cropping
|
9
|
+
# @!visibility private
|
10
|
+
class Straighten < ImageOperation
|
11
|
+
attr_accessor :angle
|
12
|
+
|
13
|
+
def call(pixbuf)
|
14
|
+
return pixbuf if angle.zero?
|
15
|
+
|
16
|
+
rotation_value_rad = angle * (Math::PI / 180)
|
17
|
+
|
18
|
+
ratio = pixbuf.width.to_f / pixbuf.height
|
19
|
+
rh = pixbuf.height / ((ratio * Math.sin(rotation_value_rad.abs)) + Math.cos(rotation_value_rad.abs))
|
20
|
+
scale = pixbuf.height / rh.to_f.abs
|
21
|
+
|
22
|
+
a_ratio = pixbuf.height.to_f / pixbuf.width
|
23
|
+
a_rh = pixbuf.width / ((a_ratio * Math.sin(rotation_value_rad.abs)) + Math.cos(rotation_value_rad.abs))
|
24
|
+
a_scale = pixbuf.width / a_rh.to_f.abs
|
25
|
+
|
26
|
+
scale = a_scale if a_scale > scale
|
27
|
+
|
28
|
+
create_pixbuf_from_image_surface(:rgb24, pixbuf.width, pixbuf.height) do |cr|
|
29
|
+
cr.translate(pixbuf.width / 2.0, pixbuf.height / 2.0)
|
30
|
+
cr.rotate(rotation_value_rad)
|
31
|
+
cr.scale(scale, scale)
|
32
|
+
cr.translate(pixbuf.width / -2.0, pixbuf.height / - 2.0)
|
33
|
+
cr.set_source_pixbuf(pixbuf)
|
34
|
+
|
35
|
+
cr.rectangle(0, 0, pixbuf.width, pixbuf.height)
|
36
|
+
cr.paint(1.0)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gdk_pixbuf2'
|
4
|
+
|
5
|
+
# GdkPixbuf module / hierachy
|
6
|
+
module GdkPixbuf
|
7
|
+
# Add #to_cairo_image_surface for converting pixels to Cairo::ImageSurface format (RGBA->ARGB)
|
8
|
+
class Pixbuf
|
9
|
+
def to_cairo_image_surface
|
10
|
+
GdkPixbufCairo.pixbuf_to_surface(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Proportionally scales down the image so that it fits within max_size*max_size square
|
14
|
+
def scale_max(max_size, interp = GdkPixbuf::InterpType::BILINEAR, _max_scale = 1.0)
|
15
|
+
mul = (max_size / [width, height].max.to_f)
|
16
|
+
mul = [1.0, mul].min
|
17
|
+
scale(width * mul, height * mul, interp)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|