morandi 0.99.03 → 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 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
  - - ">="