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 +7 -0
- data/README.md +247 -0
- data/dragonfly-lossless_rotate.gemspec +23 -0
- data/lib/dragonfly/lossless_rotate.rb +93 -0
- data/lib/dragonfly/lossless_rotate/version.rb +5 -0
- metadata +65 -0
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
|
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: []
|