morandi 0.99.03 → 0.99.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a794ac03476df828589c8239af24558daba1b32c4db9052f1dcf3704442eae43
4
- data.tar.gz: 71fb0af31081d29907392d57ceb7adda9fc6b277835fd7540a9c91a6b1b9c90c
3
+ metadata.gz: 72cf19456503d11e3729ece721a4f8cf542ec674d5c35497d53efc8d333ebbd4
4
+ data.tar.gz: 3d05af03a553609b7bb4901071b675273127a329b323fb45bd1d7c071fe8aed5
5
5
  SHA512:
6
- metadata.gz: be85f2a459dad73012fe696f285f1eae17b098e48002f5da9cf868ed876854b915115e1e6f85929948427208d2b3363abe3d7b492b6187e729bf408ad6fccb95
7
- data.tar.gz: 78beadc94f1387ff9af6d9c00d2f518f17d71f241880a5d2a828568dfaa2eb1b2aade57b198fc7f8ceeae429c1445d1c9c36c9fd461c84b3516e88964c32bf19
6
+ metadata.gz: fa60e3b5ccefa7a76fa8e4f70d28f3ef1ad80e81a8f1d07c35a258e6748e9a4ce4e5923f82be682456a74c5ca8acc185030a783f925246bf04fad5776d20aed1
7
+ data.tar.gz: 9bfff0edf6d1d52d2be6f836138b60d35c22a788fc337c46c7837a9b719f91ca6347ff746bd79f757737d6f6abba5615dd9f480e79ed07a8e9d67085cf6e3ad2
data/CHANGELOG.md CHANGED
@@ -4,7 +4,25 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## Unreleased
7
+ ## [0.99.4] 18.11.2024
8
+ ### Added
9
+ - Better test coverage for straighten operation
10
+ - Support for visual image comparison in specs
11
+ - Automated visual comparison of images in specs, also serving as a record of exact rendering behaviour
12
+ - Development scripts for performing benchmarks
13
+ - Basic test coverage for transparency support
14
+
15
+ ### Changed
16
+ - Extracted image operations to separate files within a dedicated module
17
+ - [BREAKING] Introduced raising Morandi's own errors instead of bubbling Pixbuf's
18
+
19
+ ### Fixed
20
+ - Updated required ruby version in gemspec to reflect dropping Ruby 2.3 support
21
+
22
+ ### Removed
23
+ - [BREAKING] `config` accessor in ImageProcessor removed in favour of `user_options` supplied in constructor
24
+
25
+ ## [0.99.03] 19.09.2024
8
26
  ### Added
9
27
  - Copied pixbufutils and redeye gems into main gem
10
28
  - Added gdk_pixbuf_cairo C extension to convert between GdkPixbufs and ImageSurfaces
@@ -12,15 +30,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
12
30
  - Bumped version to 0.99.01 in preparation for a 1.0 release
13
31
 
14
32
  ### Removed
15
- - support for Ruby 2.0 (and illusion of it being tested by CI)
16
- - support for Ruby 2.3
33
+ - [BREAKING] support for Ruby 2.0 (and illusion of it being tested by CI)
34
+ - [BREAKING] support for Ruby 2.3
17
35
  - gtk2 dependency
18
36
 
19
37
  ## [0.13.0] 16.12.2020
20
38
  ### Fixed
21
39
  - Refactored test suite
22
40
  - Fixed most rubocop offenses
23
- ### Aded
41
+ ### Added
24
42
  - CI pipeline
25
43
  - rubocop
26
44
  - Development image
@@ -32,7 +50,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
32
50
  ## [0.12.0] 10.12.2020
33
51
  ### Fixed
34
52
  - Compatability with gdk_pixbuf v3.4.0+ [TECH-14001]
35
- ### Aded
53
+ ### Added
36
54
  - .ruby-version file
37
55
 
38
56
 
data/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # Morandi
2
2
 
3
- Library of simple image manipulations - replicating the behaviour of
4
- morandi-js.
3
+ Library of simple image manipulations - replicating the behaviour of morandi-js.
5
4
 
6
5
  ## Installation
7
6
 
8
- Install `liblcms2-utils` to provide the `jpgicc` command used by `Morandi::ProfiledPixbuf`. Also ensure that your host system has `imagemagick` installed, which is required bi the `colorscore` gem.
7
+ Install `liblcms2-utils` to provide the `jpgicc` command used by `Morandi::ProfiledPixbuf`. Also ensure that your host system has `imagemagick` installed, which is required by the `colorscore` gem.
9
8
 
10
9
  Add this line to your application's Gemfile:
11
10
 
@@ -22,26 +21,10 @@ Or install it yourself as:
22
21
  ## Usage
23
22
 
24
23
  ````
25
- Morandi.process(in_file, settings, out_file)
24
+ Morandi.process(source, options, target_path)
26
25
  ````
27
- - in_file is a string
28
- - settings is a hash
29
- - out_file is a string
30
-
31
- Settings Key | Values | Description
32
- -------------|--------|---------------
33
- brighten | Integer -20..20 | Change image brightness
34
- gamma | Float | Gamma correct image
35
- contrast | Integer -20..20 | Change image contrast
36
- sharpen | Integer -5..5 | Sharpen / Blur (negative value)
37
- redeye | Array[[Integer,Integer],...] | Apply redeye correction at point
38
- angle | Integer 0,90,180,270 | Rotate image
39
- straighten | Float | Rotate by N degrees and zoom
40
- crop | Array[Integer,Integer,Integer,Integer] | Crop image
41
- fx | String greyscale,sepia,bluetone | Apply colour filters
42
- border-style | String square,retro | Set border style
43
- background-style | String retro,black,white | Set border colour
44
- quality | String '1'..'100' | Set JPG compression value, defaults to 97%
26
+
27
+ For the detailed documentation of options see `lib/morandi.rb`
45
28
 
46
29
  ## Contributing
47
30
 
@@ -53,4 +36,4 @@ quality | String '1'..'100' | Set JPG compression value, defaults to 97%
53
36
 
54
37
  ### Development
55
38
 
56
- Since this gem depends on the `liblcms2-utils` library, which can be awkward to install on some operating systems, we also provide a development docker image. A Makefile is also provided as a simple CLI. To build the image and run the container, type `make` from the project root. The container itself runs `guard` as its main process. Running the container via `make` will drop you into the guard prompt, which will run the test suite whenever any of the source code or tests are changed. The tests can be kicked-off manually via the `all` command at the guard prompt. Individual test can be run using the `focus: true` annotation on an example or describe block. If you need to access a bash shell in the container (for example, to run rubocop), use the command `make shell`.
39
+ Since this gem depends on the `liblcms2-utils` library, which can be awkward to install on some operating systems, we also provide a development docker image. A Makefile is also provided as a simple CLI. To build the image and run the container, type `make` from the project root. The container itself runs `guard` as its main process. Running the container via `make` will drop you into the guard prompt, which will run the test suite whenever any of the source code or tests are changed. The tests can be kicked-off manually via the `all` command at the guard prompt. Individual test can be run using the `focus: true` annotation on an example or describe block. If you need to access a bash shell in the container (for example, to run rubocop), use the command `make shell`.
Binary file
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Morandi
4
+ class Error < StandardError
5
+ end
6
+
7
+ class CorruptImageError < Error
8
+ end
9
+
10
+ class UnknownTypeError < Error
11
+ end
12
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'colorscore'
4
-
5
3
  module Morandi
6
4
  # Base Image Op class
7
5
  # @!visibility private
@@ -30,176 +28,4 @@ module Morandi
30
28
  final_pb
31
29
  end
32
30
  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
31
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'morandi/profiled_pixbuf'
4
4
  require 'morandi/redeye'
5
+ require 'morandi/operation/straighten'
6
+ require 'morandi/operation/colourify'
7
+ require 'morandi/operation/image_border'
5
8
 
6
9
  module Morandi
7
10
  # rubocop:disable Metrics/ClassLength
@@ -9,7 +12,6 @@ module Morandi
9
12
  # ImageProcessor transforms an image.
10
13
  class ImageProcessor
11
14
  attr_reader :options, :pb
12
- attr_accessor :config
13
15
 
14
16
  def initialize(file, user_options, local_options = {})
15
17
  @file = file
@@ -20,7 +22,7 @@ module Morandi
20
22
  @options = (local_options || {}).merge(user_options || {})
21
23
  @local_options = local_options
22
24
 
23
- @scale_to = @options['output.max']
25
+ @max_size_px = @options['output.max']
24
26
  @width = @options['output.width']
25
27
  @height = @options['output.height']
26
28
  end
@@ -55,6 +57,10 @@ module Morandi
55
57
  @pb = @pb.scale_max([@width, @height].max) if @options['output.limit'] && @width && @height
56
58
 
57
59
  @pb
60
+ rescue GdkPixbuf::PixbufError::UnknownType => e
61
+ raise UnknownTypeError, e.message
62
+ rescue GdkPixbuf::PixbufError::CorruptImage => e
63
+ raise CorruptImageError, e.message
58
64
  end
59
65
 
60
66
  # Returns generated pixbuf
@@ -84,16 +90,18 @@ module Morandi
84
90
 
85
91
  def get_pixbuf
86
92
  _, width, height = GdkPixbuf::Pixbuf.get_file_info(@file)
87
- @pb = Morandi::ProfiledPixbuf.new(@file, @local_options, @scale_to)
88
- @actual_max = [@pb.width, @pb.height].max
89
-
90
- @src_max = if @scale_to
91
- [width, height].max
92
- else
93
- [@pb.width, @pb.height].max
94
- end
95
-
96
- @scale = @actual_max / @src_max.to_f
93
+ @pb = Morandi::ProfiledPixbuf.new(@file, @local_options, @max_size_px)
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
103
+
104
+ @scale = actual_max / src_max.to_f
97
105
  end
98
106
 
99
107
  SHARPEN = [
@@ -159,8 +167,7 @@ module Morandi
159
167
  @pb = @pb.rotate(a) unless (a % 360).zero?
160
168
 
161
169
  unless options['straighten'].to_f.zero?
162
- @pb = Morandi::Straighten.new_from_hash(angle: options['straighten'].to_f).call(nil,
163
- @pb)
170
+ @pb = Morandi::Operation::Straighten.new_from_hash(angle: options['straighten'].to_f).call(@pb)
164
171
  end
165
172
 
166
173
  @image_width = @pb.width
@@ -172,7 +179,6 @@ module Morandi
172
179
  }.freeze
173
180
  def config_for(key)
174
181
  return options[key] if options&.key?(key)
175
- return @config[key] if @config&.key?(key)
176
182
 
177
183
  DEFAULT_CONFIG[key]
178
184
  end
@@ -203,11 +209,11 @@ module Morandi
203
209
 
204
210
  case filter
205
211
  when 'greyscale', 'sepia', 'bluetone'
206
- op = Morandi::Colourify.new_from_hash('filter' => filter)
212
+ op = Morandi::Operation::Colourify.new_from_hash('filter' => filter)
207
213
  else
208
214
  return
209
215
  end
210
- @pb = op.call(nil, @pb)
216
+ @pb = op.call(@pb)
211
217
  end
212
218
 
213
219
  def apply_decorations!
@@ -222,7 +228,7 @@ module Morandi
222
228
  crop = options['crop']
223
229
  crop = crop.map { |s| (s.to_f * @scale).floor } if crop && not_equal_to_one(@scale)
224
230
 
225
- op = Morandi::ImageBorder.new_from_hash(
231
+ op = Morandi::Operation::ImageBorder.new_from_hash(
226
232
  'style' => style,
227
233
  'colour' => colour || '#000000',
228
234
  'crop' => crop,
@@ -232,7 +238,7 @@ module Morandi
232
238
  'border_size' => @scale * config_for('border-size-mm').to_i * 300 / 25.4 # 5mm at 300dpi
233
239
  )
234
240
 
235
- @pb = op.call(nil, @pb)
241
+ @pb = op.call(@pb)
236
242
  end
237
243
 
238
244
  private
@@ -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
@@ -10,6 +10,7 @@ module GdkPixbuf
10
10
  GdkPixbufCairo.pixbuf_to_surface(self)
11
11
  end
12
12
 
13
+ # Proportionally scales down the image so that it fits within max_size*max_size square
13
14
  def scale_max(max_size, interp = GdkPixbuf::InterpType::BILINEAR, _max_scale = 1.0)
14
15
  mul = (max_size / [width, height].max.to_f)
15
16
  mul = [1.0, mul].min
@@ -1,61 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'gdk_pixbuf2'
4
+ require 'morandi/srgb_conversion'
4
5
 
5
6
  module Morandi
6
7
  # ProfiledPixbuf is a descendent of GdkPixbuf::Pixbuf with ICC support.
7
8
  # It attempts to load an image using jpegicc/littlecms to ensure that it is sRGB.
9
+ # NOTE: pixbuf supports colour profiles, but it requires an explicit icc-profile option to embed it when saving file
8
10
  class ProfiledPixbuf < GdkPixbuf::Pixbuf
9
- def valid_jpeg?(filename)
10
- return false unless File.exist?(filename)
11
- return false unless File.size(filename).positive?
12
-
13
- type, = GdkPixbuf::Pixbuf.get_file_info(filename)
14
-
15
- type && type.name.eql?('jpeg')
16
- rescue StandardError
17
- false
18
- end
19
-
20
- # TODO: this doesn't use lcms
21
- def self.from_string(string, loader: nil, chunk_size: 4096)
22
- loader ||= GdkPixbuf::PixbufLoader.new
23
- ((string.bytesize + chunk_size - 1) / chunk_size).times do |i|
24
- loader.write(string.byteslice(i * chunk_size, chunk_size))
25
- end
26
- loader.close
27
- loader.pixbuf
28
- end
29
-
30
- def self.default_icc_path(path)
31
- "#{path}.icc.jpg"
32
- end
33
-
34
- def initialize(file, local_options, scale_to = nil)
11
+ def initialize(path, local_options, max_size_px = nil)
35
12
  @local_options = local_options
36
- @file = file
37
13
 
38
- if suitable_for_jpegicc?
39
- icc_file = icc_cache_path
40
- valid_jpeg?(icc_file) || system('jpgicc', '-q97', @file, icc_file)
41
- file = icc_file if valid_jpeg?(icc_file)
42
- end
14
+ path = srgb_path(path) || path
43
15
 
44
- if scale_to
45
- super(file: file, width: scale_to, height: scale_to)
16
+ if max_size_px
17
+ super(file: path, width: max_size_px, height: max_size_px)
46
18
  else
47
- super(file: file)
19
+ super(file: path)
48
20
  end
49
21
  end
50
22
 
51
23
  private
52
24
 
53
- def suitable_for_jpegicc?
54
- valid_jpeg?(@file)
55
- end
56
-
57
- def icc_cache_path
58
- @local_options['path.icc'] || Morandi::ProfiledPixbuf.default_icc_path(@file)
25
+ def srgb_path(original_path)
26
+ Morandi::SrgbConversion.perform(original_path, target_path: @local_options['path.icc'])
59
27
  end
60
28
  end
61
29
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'gdk_pixbuf2'
4
+
5
+ module Morandi
6
+ # Converts the file under `path` to sRGB colour space
7
+ class SrgbConversion
8
+ # Performs a conversion to srgb colour space if possible
9
+ # Returns a path to converted file on success or nil on failure
10
+ def self.perform(path, target_path: nil)
11
+ return unless suitable_for_jpegicc?(path)
12
+
13
+ icc_file_path = target_path || default_icc_path(path)
14
+ return icc_file_path if valid_jpeg?(icc_file_path)
15
+
16
+ system('jpgicc', '-q97', path, icc_file_path, out: '/dev/null', err: '/dev/null')
17
+
18
+ return unless valid_jpeg?(icc_file_path)
19
+
20
+ icc_file_path
21
+ end
22
+
23
+ def self.default_icc_path(path)
24
+ "#{path}.icc.jpg"
25
+ end
26
+
27
+ def self.valid_jpeg?(path)
28
+ return false unless File.exist?(path)
29
+ return false unless File.size(path).positive?
30
+
31
+ type, = GdkPixbuf::Pixbuf.get_file_info(path)
32
+
33
+ type && type.name.eql?('jpeg')
34
+ rescue StandardError
35
+ false
36
+ end
37
+
38
+ def self.suitable_for_jpegicc?(path)
39
+ valid_jpeg?(path)
40
+ end
41
+ end
42
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Morandi
4
- VERSION = '0.99.03'
4
+ VERSION = '0.99.4'
5
5
  end
data/lib/morandi.rb CHANGED
@@ -5,8 +5,8 @@ require 'morandi_native'
5
5
 
6
6
  require 'morandi/cairo_ext'
7
7
  require 'morandi/pixbuf_ext'
8
+ require 'morandi/errors'
8
9
  require 'morandi/image_processor'
9
- require 'morandi/image_operation'
10
10
  require 'morandi/redeye'
11
11
  require 'morandi/crop_utils'
12
12
 
@@ -14,29 +14,37 @@ require 'morandi/crop_utils'
14
14
  module Morandi
15
15
  module_function
16
16
 
17
- # The main entry point for the libray
17
+ # The main entry point for the library
18
18
  #
19
- # @param in_file [String|GdkPixbuf::Pixbuf] source image
20
- # @param settings [Hash]
21
- # @param out_file [String] target location for image
22
- # @param local_options [Hash]
23
- #
24
- # Settings Key | Values | Description
25
- # -------------|--------|---------------
26
- # brighten | Integer -20..20 | Change image brightness
27
- # gamma | Float | Gamma correct image
28
- # contrast | Integer -20..20 | Change image contrast
29
- # sharpen | Integer -5..5 | Sharpen / Blur (negative value)
30
- # redeye | Array[[Integer,Integer],...] | Apply redeye correction at point
31
- # angle | Integer 0,90,180,270 | Rotate image
32
- # crop | Array[Integer,Integer,Integer,Integer] | Crop image
33
- # fx | String greyscale,sepia,bluetone | Apply colour filters
34
- # border-style | String square,retro | Set border style
35
- # background-style | String retro,black,white | Set border colour
36
- # quality | String '1'..'100' | Set JPG compression value, defaults to 97%
37
- def process(file_in, options, file_out, local_options = {})
38
- pro = ImageProcessor.new(file_in, options, local_options)
19
+ # @param source [String|GdkPixbuf::Pixbuf] source image
20
+ # @param [Hash] options The options describing expected processing to perform
21
+ # @option options [Integer] 'brighten' Change image brightness (-20..20)
22
+ # @option options [Float] 'gamma' Gamma correct image
23
+ # @option options [Integer] 'contrast' Change image contrast (-20..20)
24
+ # @option options [Integer] 'sharpen' Sharpen (1..5) / Blur (-1..-5)
25
+ # @option options [Array[[Integer,Integer],...]] 'redeye' Apply redeye correction at point
26
+ # @option options [Integer] 'angle' Rotate image clockwise by multiple of 90 (0, 90, 180, 270)
27
+ # @option options [Array[Integer,Integer,Integer,Integer]] 'crop' Crop image (x, y, width, height)
28
+ # @option options [String] 'fx' Apply colour filters ('greyscale', 'sepia', 'bluetone')
29
+ # @option options [String] 'border-style' Set border style ('square', 'retro')
30
+ # @option options [String] 'background-style' Set border colour ('retro', 'black', 'white', 'dominant')
31
+ # @option options [Integer] 'quality' (97) Set JPG compression value (1 to 100)
32
+ # @option options [Integer] 'output.max' Downscales the image to fit within the square of given size before
33
+ # processing to limit the required resources
34
+ # @option options [Integer] 'output.width' Sets desired width of resulting image
35
+ # @option options [Integer] 'output.height' Sets desired height of resulting image
36
+ # @option options [TrueClass|FalseClass] 'image.auto-crop' (true) If the output dimensions are set and this is true,
37
+ # image is cropped automatically to the desired
38
+ # dimensions.
39
+ # @option options [TrueClass|FalseClass] 'output.limit' (false) If the output dimensions are defined and this is true,
40
+ # the output image is scaled down to fit within square of
41
+ # size of the longer edge (ignoring shorter dimension!)
42
+ # @param target_path [String] target location for image
43
+ # @param local_options [Hash] Hash of options other than desired transformations
44
+ # @option local_options [String] 'path.icc' A path to store the input after converting to sRGB colour space
45
+ def process(source, options, target_path, local_options = {})
46
+ pro = ImageProcessor.new(source, options, local_options)
39
47
  pro.result
40
- pro.write_to_jpeg(file_out)
48
+ pro.write_to_jpeg(target_path)
41
49
  end
42
50
  end
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: morandi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.99.03
4
+ version: 0.99.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - |+
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2024-09-19 00:00:00.000000000 Z
14
+ date: 2024-11-22 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: atk
@@ -122,11 +122,16 @@ files:
122
122
  - lib/morandi.rb
123
123
  - lib/morandi/cairo_ext.rb
124
124
  - lib/morandi/crop_utils.rb
125
+ - lib/morandi/errors.rb
125
126
  - lib/morandi/image_operation.rb
126
127
  - lib/morandi/image_processor.rb
128
+ - lib/morandi/operation/colourify.rb
129
+ - lib/morandi/operation/image_border.rb
130
+ - lib/morandi/operation/straighten.rb
127
131
  - lib/morandi/pixbuf_ext.rb
128
132
  - lib/morandi/profiled_pixbuf.rb
129
133
  - lib/morandi/redeye.rb
134
+ - lib/morandi/srgb_conversion.rb
130
135
  - lib/morandi/version.rb
131
136
  - lib/morandi_native.so
132
137
  homepage: https://github.com/livelink/morandi-rb
@@ -143,7 +148,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
143
148
  requirements:
144
149
  - - ">="
145
150
  - !ruby/object:Gem::Version
146
- version: '2.0'
151
+ version: '2.7'
147
152
  required_rubygems_version: !ruby/object:Gem::Requirement
148
153
  requirements:
149
154
  - - ">="