dragonfly-lossless_rotate 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4570113d02eed8571af6ccd48135730ecb435d85edc5db4f3c12dc5134fbb4c0
4
+ data.tar.gz: 1c711083d0e1a89a0f13ade6e1d30488f4f565aaefda54fcde30c73b356e4b94
5
+ SHA512:
6
+ metadata.gz: 83923e26bc11d4cab7cbf1dfc30f1a81528917ff5454d5636eeb0d8a9920454639a6fa74c94820b84e804da5aa3ffd3ffa6c63c033284fd90ce839be8291743a
7
+ data.tar.gz: 8dd6253e9ac9f9766439c87f26c285c0f8be8bfc1d7bba6ba92ca92b773cb5ab72d4b611f914425839d24ce62d4486643c477b9ad82a9262dab2edd275b3fc10
data/README.md ADDED
@@ -0,0 +1,247 @@
1
+ # Dragonfly Lossless Rotate
2
+
3
+ **About 60% more performance with libjpeg-turbo tools**
4
+
5
+ ## NOTE
6
+
7
+ Tool _jpegtran_ from MozJPEG may work incorrectly and not rotate same image many times.
8
+ You should test it before run in production.
9
+
10
+ ## Setup
11
+
12
+ ```ruby
13
+ gem "dragonfly-lossless_rotate"
14
+ ```
15
+
16
+ ```ruby
17
+ Dragonfly.app.configure
18
+ require "dragonfly/lossless_rotate"
19
+ plugin :lossless_rotate
20
+ end
21
+ ```
22
+
23
+ ## Requirements
24
+
25
+ By default gem use _libjpeg_ binaries:
26
+ ```shell
27
+ cjpeg
28
+ djpeg
29
+ jpegtran
30
+ pnmflip
31
+ ```
32
+
33
+ But you can set MozJPEG binaries in ENV `CJPEG_BIN=mozjpeg-cjpeg` or in config:
34
+ ```ruby
35
+ Dragonfly.app.configure
36
+ require "dragonfly/lossless_rotate"
37
+ plugin :lossless_rotate, cjpeg_bin: "mozjpeg-cjpeg",
38
+ djpeg_bin: "mozjpeg-djpeg",
39
+ jpegtran_bin: "mozjpeg-jpegtran"
40
+
41
+ end
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ JPEG only:
47
+ ```ruby
48
+ @image.process(:lossless_rotate) # default 90
49
+ @image.process(:lossless_rotate, 180)
50
+ @image.process(:lossless_rotate, 270)
51
+ @image.process(:lossless_rotate, -90)
52
+
53
+ # Without JPEG optimization
54
+ @image.process(:lossless_rotate, 90, optimize: false)
55
+ ```
56
+
57
+ With fallback for other formats (rotate via ImageMagick):
58
+ ```ruby
59
+ @image.process(:safe_lossless_rotate)
60
+ ```
61
+
62
+ ## Benchmark
63
+
64
+ - _libjpeg_ version 9b (17-Jan-2016)
65
+ - _libjpeg-turbo_ version 1.4.2 (build 20160222)
66
+ - _MozJPEG_ version 3.3.2 (build 20180713)
67
+
68
+ JPEG 85KB 552x416px
69
+
70
+ ### ImageMagick rotate
71
+ ```bash
72
+ convert old_path -rotate 90 new_path
73
+ ```
74
+ ```ruby
75
+ puts Benchmark.measure { 500.times { @image.rotate(90).apply } }
76
+ 0.360000 1.570000 25.270000 ( 25.168681)
77
+ ```
78
+
79
+ ### Lossless rotate
80
+
81
+ #### libjpeg
82
+
83
+ ##### (optimize=true)
84
+ ```bash
85
+ jpegtran -rotate 90 -perfect -optimize old_path > new_path
86
+ ```
87
+
88
+ ```ruby
89
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate).apply } }
90
+ 0.240000 1.190000 10.600000 ( 11.368903)
91
+
92
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate).apply } }
93
+ 0.440000 1.640000 24.360000 ( 26.211808)
94
+ ```
95
+
96
+ ##### (optimize=false)
97
+ ```bash
98
+ jpegtran -rotate 90 -perfect old_path > new_path
99
+ ```
100
+
101
+ ```ruby
102
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate, 90, optimize: false).apply } }
103
+ 0.290000 1.170000 9.600000 ( 10.321087)
104
+
105
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate, 90, optimize: false).apply } }
106
+ 0.450000 1.470000 23.610000 ( 25.478872)
107
+ ```
108
+
109
+ #### libjpeg-turbo
110
+
111
+ ##### (optimize=true)
112
+ ```bash
113
+ jpegtran -rotate 90 -perfect -optimize old_path > new_path
114
+ ```
115
+
116
+ ```ruby
117
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate).apply } }
118
+ 0.280000 1.160000 9.170000 ( 9.876645)
119
+
120
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate).apply } }
121
+ 0.560000 1.780000 22.710000 ( 23.879913)
122
+ ```
123
+
124
+ ##### (optimize=false)
125
+ ```bash
126
+ jpegtran -rotate 90 -perfect old_path > new_path
127
+ ```
128
+
129
+ ```ruby
130
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate, 90, optimize: false).apply } }
131
+ 0.290000 1.170000 9.600000 ( 10.321087)
132
+
133
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate, 90, optimize: false).apply } }
134
+ 0.450000 1.470000 23.610000 ( 25.478872)
135
+ ```
136
+
137
+ #### MozJPEG
138
+
139
+ ##### (optimize=true)
140
+ ```bash
141
+ mozjpeg-jpegtran -rotate 90 -perfect -optimize old_path > new_path
142
+ ```
143
+
144
+ ```ruby
145
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate).apply } }
146
+ 0.270000 1.110000 35.230000 ( 36.693039)
147
+
148
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate).apply } }
149
+ 0.550000 1.540000 48.880000 ( 50.171667)
150
+ ```
151
+
152
+ ##### (optimize=false)
153
+ ```bash
154
+ mozjpeg-jpegtran -rotate 90 -perfect old_path > new_path
155
+ ```
156
+
157
+ ```ruby
158
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate, 90, optimize: false).apply } }
159
+ 0.310000 1.100000 34.960000 ( 35.947432)
160
+
161
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate, 90, optimize: false).apply } }
162
+ 0.470000 1.660000 49.050000 ( 50.823576)
163
+ ```
164
+
165
+ ### Fallback when jpegtran transformation is not perfect
166
+
167
+ > if the image dimensions are not a multiple of the iMCU size (usually 8 or 16 pixels)
168
+
169
+ Same image but resized to 556x417px
170
+
171
+ #### libjpeg
172
+
173
+ ##### (optimize=true)
174
+ ```bash
175
+ djpeg old_path | pnmflip -r270 | cjpeg -optimize > new_path
176
+ ```
177
+ ```ruby
178
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate).apply } }
179
+ 0.340000 0.930000 19.320000 ( 16.055059)
180
+
181
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate).apply } }
182
+ 0.390000 1.240000 32.520000 ( 30.055926)
183
+ ```
184
+
185
+ ##### (optimize=false)
186
+ ```bash
187
+ djpeg old_path | convert - -rotate 90 JPG:new_path
188
+ ```
189
+ ```ruby
190
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate, 90, optimize: false).apply } }
191
+ 0.310000 1.450000 30.520000 ( 28.357557)
192
+
193
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate, 90, optimize: false).apply } }
194
+ 0.530000 1.520000 43.790000 ( 41.732961)
195
+ ```
196
+
197
+ #### libjpeg-turbo
198
+
199
+ ##### (optimize=true)
200
+ ```bash
201
+ djpeg old_path | pnmflip -r270 | cjpeg -optimize > new_path
202
+ ```
203
+ ```ruby
204
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate).apply } }
205
+ 0.410000 1.280000 16.310000 ( 13.220535)
206
+
207
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate).apply } }
208
+ 0.310000 1.330000 30.300000 ( 28.332533)
209
+ ```
210
+
211
+ ##### (optimize=false)
212
+ ```bash
213
+ djpeg old_path | convert - -rotate 90 JPG:new_path
214
+ ```
215
+ ```ruby
216
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate, 90, optimize: false).apply } }
217
+ 0.470000 1.110000 29.630000 ( 26.944807)
218
+
219
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate, 90, optimize: false).apply } }
220
+ 0.500000 1.810000 43.420000 ( 42.309805)
221
+ ```
222
+
223
+ #### MozJPEG
224
+
225
+ ##### (optimize=true)
226
+ ```bash
227
+ mozjpeg-djpeg old_path | pnmflip -r270 | mozjpeg-cjpeg -optimize > new_path
228
+ ```
229
+ ```ruby
230
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate).apply } }
231
+ 0.400000 1.150000 41.190000 ( 37.970843)
232
+
233
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate).apply } }
234
+ 0.420000 1.670000 55.700000 ( 52.835614)
235
+ ```
236
+
237
+ ##### (optimize=false)
238
+ ```bash
239
+ mozjpeg-djpeg old_path | convert - -rotate 90 JPG:new_path
240
+ ```
241
+ ```ruby
242
+ puts Benchmark.measure { 500.times { @image.process(:lossless_rotate, 90, optimize: false).apply } }
243
+ 0.270000 1.170000 31.300000 ( 28.983087)
244
+
245
+ puts Benchmark.measure { 500.times { @image.process(:safe_lossless_rotate, 90, optimize: false).apply } }
246
+ 0.340000 1.310000 44.890000 ( 43.052428)
247
+ ```
@@ -0,0 +1,23 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "dragonfly/lossless_rotate/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "dragonfly-lossless_rotate"
7
+ spec.version = Dragonfly::LosslessRotate::VERSION
8
+ spec.summary = "Dragonfly lossless image rotate"
9
+ spec.description = "Lossless rotating and compressing JPEG images via libjpeg tools"
10
+ spec.author = "Maxim Perepelitsa"
11
+ spec.email = "n0xff@outlook.com"
12
+ spec.homepage = "https://github.com/n0xff/dragonfly-lossless_rotate"
13
+ spec.license = "MIT"
14
+
15
+ spec.requirements << "cjpeg"
16
+ spec.requirements << "djpeg"
17
+ spec.requirements << "jpegtran"
18
+ spec.requirements << "pnmflip"
19
+
20
+ spec.files = `git ls-files`.split($/)
21
+
22
+ spec.add_runtime_dependency "dragonfly", "~> 1.0"
23
+ end
@@ -0,0 +1,93 @@
1
+ require "dragonfly"
2
+
3
+ Dragonfly::App.register_plugin(:lossless_rotate) { Dragonfly::LosslessRotate::Plugin.new }
4
+
5
+ module Dragonfly
6
+ class LosslessRotate
7
+
8
+ class Plugin
9
+ def call(app, opts={})
10
+ app.env[:cjpeg_bin] = opts[:cjpeg_bin] || "cjpeg"
11
+ app.env[:djpeg_bin] = opts[:djpeg_bin] || "djpeg"
12
+ app.env[:jpegtran_bin] = opts[:jpegtran_bin] || "jpegtran"
13
+ app.env[:pnmflip_bin] = opts[:pnmflip_bin] || "pnmflip"
14
+
15
+ app.add_processor :lossless_rotate, Dragonfly::LosslessRotate::Rotate.new
16
+ app.add_processor :safe_lossless_rotate, Dragonfly::LosslessRotate::SafeRotate.new
17
+ end
18
+ end
19
+
20
+ # Only JPEG format
21
+ class Rotate
22
+ def call(content, degree=90, optimize: true)
23
+ unless [90, 180, 270, -90, -180, -270].include?(degree)
24
+ warn "Rotate by 90, 180 and 270 degrees allowed"
25
+ degree = 90
26
+ end
27
+
28
+ rotate(content, degree, optimize)
29
+ end
30
+
31
+ private
32
+
33
+ def rotate(content, degree, optimize)
34
+ cjpeg_bin = content.env[:cjpeg_bin] || "cjpeg"
35
+ djpeg_bin = content.env[:djpeg_bin] || "djpeg"
36
+ jpegtran_bin = content.env[:jpegtran_bin] || "jpegtran"
37
+ pnmflip_bin = content.env[:pnmflip_bin] || "pnmflip"
38
+
39
+ content.shell_update escape: false do |old_path, new_path|
40
+ optimize_option = " -optimize" if optimize
41
+ output_command = if optimize
42
+ " #{pnmflip_bin} -r#{pnmflip_degrees(degree)} | #{cjpeg_bin}#{optimize_option} > #{new_path}"
43
+ else
44
+ " convert - -rotate #{degree} JPG:#{new_path}"
45
+ end
46
+
47
+ lossless_rotate_command = "#{jpegtran_bin} -rotate #{jpegtran_degrees(degree)} -perfect#{optimize_option} #{old_path} > #{new_path}"
48
+ lossy_rotate_command = "#{djpeg_bin} #{old_path} |#{output_command}"
49
+
50
+ "#{lossless_rotate_command} || #{lossy_rotate_command}"
51
+ end
52
+ end
53
+
54
+ def pnmflip_degrees(degree)
55
+ {
56
+ 90 => 270,
57
+ 180 => 180,
58
+ 270 => 90,
59
+ -90 => 90,
60
+ -180 => 180,
61
+ -270 => 270
62
+ }[degree]
63
+ end
64
+
65
+ def jpegtran_degrees(degree)
66
+ {
67
+ 90 => 90,
68
+ 180 => 180,
69
+ 270 => 180,
70
+ -90 => 270,
71
+ -180 => 180,
72
+ -270 => 90
73
+ }[degree]
74
+ end
75
+
76
+ def jpeg?(content)
77
+ content.shell_eval { |path| "identify -format \%m #{path}" } == "JPEG"
78
+ end
79
+ end
80
+
81
+ # All formats support by ImageMagick
82
+ class SafeRotate < Rotate
83
+ def rotate(content, degree, optimize)
84
+ return super if jpeg?(content)
85
+
86
+ content.shell_update escape: false do |old_path, new_path|
87
+ "convert -rotate #{degree} #{old_path} > #{new_path}"
88
+ end
89
+ end
90
+ private :rotate
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,5 @@
1
+ module Dragonfly
2
+ class LosslessRotate
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dragonfly-lossless_rotate
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Maxim Perepelitsa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dragonfly
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ description: Lossless rotating and compressing JPEG images via libjpeg tools
28
+ email: n0xff@outlook.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - README.md
34
+ - dragonfly-lossless_rotate.gemspec
35
+ - lib/dragonfly/lossless_rotate.rb
36
+ - lib/dragonfly/lossless_rotate/version.rb
37
+ homepage: https://github.com/n0xff/dragonfly-lossless_rotate
38
+ licenses:
39
+ - MIT
40
+ metadata: {}
41
+ post_install_message:
42
+ rdoc_options: []
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements:
56
+ - cjpeg
57
+ - djpeg
58
+ - jpegtran
59
+ - pnmflip
60
+ rubyforge_project:
61
+ rubygems_version: 2.7.7
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Dragonfly lossless image rotate
65
+ test_files: []