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 +4 -4
- data/CHANGELOG.md +23 -5
- data/README.md +6 -23
- data/lib/gdk_pixbuf_cairo.so +0 -0
- data/lib/morandi/errors.rb +12 -0
- data/lib/morandi/image_operation.rb +0 -174
- data/lib/morandi/image_processor.rb +25 -19
- 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 +1 -0
- data/lib/morandi/profiled_pixbuf.rb +9 -41
- data/lib/morandi/srgb_conversion.rb +42 -0
- data/lib/morandi/version.rb +1 -1
- data/lib/morandi.rb +31 -23
- data/lib/morandi_native.so +0 -0
- metadata +8 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72cf19456503d11e3729ece721a4f8cf542ec674d5c35497d53efc8d333ebbd4
|
|
4
|
+
data.tar.gz: 3d05af03a553609b7bb4901071b675273127a329b323fb45bd1d7c071fe8aed5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
##
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
|
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(
|
|
24
|
+
Morandi.process(source, options, target_path)
|
|
26
25
|
````
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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`.
|
data/lib/gdk_pixbuf_cairo.so
CHANGED
|
Binary file
|
|
@@ -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
|
-
@
|
|
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, @
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
data/lib/morandi/pixbuf_ext.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
45
|
-
super(file:
|
|
16
|
+
if max_size_px
|
|
17
|
+
super(file: path, width: max_size_px, height: max_size_px)
|
|
46
18
|
else
|
|
47
|
-
super(file:
|
|
19
|
+
super(file: path)
|
|
48
20
|
end
|
|
49
21
|
end
|
|
50
22
|
|
|
51
23
|
private
|
|
52
24
|
|
|
53
|
-
def
|
|
54
|
-
|
|
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
|
data/lib/morandi/version.rb
CHANGED
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
|
|
17
|
+
# The main entry point for the library
|
|
18
18
|
#
|
|
19
|
-
# @param
|
|
20
|
-
# @param
|
|
21
|
-
# @
|
|
22
|
-
# @
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
48
|
+
pro.write_to_jpeg(target_path)
|
|
41
49
|
end
|
|
42
50
|
end
|
data/lib/morandi_native.so
CHANGED
|
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.
|
|
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-
|
|
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.
|
|
151
|
+
version: '2.7'
|
|
147
152
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
153
|
requirements:
|
|
149
154
|
- - ">="
|