dragonfly-lossless_rotate 0.1.0

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 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: []