oximg 0.18.1-aarch64-linux-gnu → 0.20.0-aarch64-linux-gnu
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/README.md +50 -8
- data/exe/oximg +0 -0
- data/lib/oximg/binary.rb +12 -0
- data/lib/oximg/native/liboximg_probe.so +0 -0
- data/lib/oximg/native.rb +5 -4
- data/lib/oximg/stripped.rb +109 -0
- data/lib/oximg/version.rb +1 -1
- data/lib/oximg.rb +100 -18
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 18c7da6b14cbd76c0f403a39479b59e611e643a15f06a087012c42e7c86114f4
|
|
4
|
+
data.tar.gz: 0237a92850bd1259b6b1eb1182a2688d4c73ff2d2e3de14b1de6138212a80201
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cbcb4f7a737d982744d982a5cd489536964e394feae912e33166a659a4f8e4b49e311a229452b39f452f66da4e2fcfa21d65e549a354806b8e86b7331de6ccde
|
|
7
|
+
data.tar.gz: d4f54f32aabd7bd4dc7659bbe2b9f9462e04bc74f82e0c0092fed1af7881f63697af8d04abb328a1fe7da47af299c9ba8b351bd1a93de78220d6dc2d50ea0461
|
data/README.md
CHANGED
|
@@ -20,7 +20,8 @@ Oximg.resize("in.jpg", "out.jpg", width: 750)
|
|
|
20
20
|
Oximg.resize("in.jpg", "out.webp", width: 750, height: 500, quality: 80)
|
|
21
21
|
Oximg.strip("upload.jpg", "clean.jpg")
|
|
22
22
|
Oximg.probe("in.jpg")
|
|
23
|
-
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000
|
|
23
|
+
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000,
|
|
24
|
+
# orientation: 1, display_width: 4000, display_height: 3000}
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
A GIF source is read and probed like the others (`format: :gif`), but
|
|
@@ -66,7 +67,7 @@ cannot be read as a flag.
|
|
|
66
67
|
## Stripping metadata
|
|
67
68
|
|
|
68
69
|
```ruby
|
|
69
|
-
Oximg.strip(source, destination, icc: nil, env: nil)
|
|
70
|
+
Oximg.strip(source, destination, icc: nil, auto_rotate: nil, env: nil)
|
|
70
71
|
```
|
|
71
72
|
|
|
72
73
|
Removes EXIF (GPS, camera, timestamps), XMP, IPTC and comments from a
|
|
@@ -78,6 +79,38 @@ still displays upright. `icc: :strip` also drops the colour profile.
|
|
|
78
79
|
The default is `:keep`, or `OXIMG_ICC`. Dropping a CMYK JPEG's profile
|
|
79
80
|
raises `Oximg::ProcessingError`, as do GIF and AVIF sources.
|
|
80
81
|
|
|
82
|
+
`auto_rotate: true` turns a rotated JPEG upright instead, still without
|
|
83
|
+
re-encoding: its compressed 8x8 blocks are moved, as `jpegtran
|
|
84
|
+
-perfect` does, so the stored and displayed sizes agree for every
|
|
85
|
+
reader, not only for ones that honor EXIF. That is only exact when the
|
|
86
|
+
edges it moves are whole 8 or 16 px blocks (a phone's 4032x3024 is);
|
|
87
|
+
otherwise, and for PNG and WebP, the file is stripped as without it,
|
|
88
|
+
orientation kept. It costs a full entropy decode and encode, about
|
|
89
|
+
170 ms for a 12 MP photo.
|
|
90
|
+
|
|
91
|
+
`strip` returns an `Oximg::Stripped`, the binary's own account of the
|
|
92
|
+
run, so telling which happened needs no second read:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
result = Oximg.strip(upload, clean, icc: :keep, auto_rotate: true)
|
|
96
|
+
result.upright? # no orientation tag left: stored size is displayed size
|
|
97
|
+
result.rotated? # the pixels were turned upright
|
|
98
|
+
result.orientation #=> 6, the tag kept, or nil
|
|
99
|
+
result.not_rotated&.code #=> :unaligned_edges, :corrupt, :over_limit or :not_jpeg
|
|
100
|
+
result.not_rotated&.message # for humans; the wording may change
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`not_rotated` is nil unless `auto_rotate: true` left a rotated source
|
|
104
|
+
tagged. The code is the contract (the table is in
|
|
105
|
+
[docs/features/paths.md](../../docs/features/paths.md)), so count
|
|
106
|
+
fallbacks by it. The result also stands in for the destination path
|
|
107
|
+
`strip` used to return: `to_s`, `to_path` and `to_str` are the path and
|
|
108
|
+
it compares equal to that String, so `File.size(Oximg.strip(...))`
|
|
109
|
+
still works. With an `oximg` older than 0.20, which has no `strip
|
|
110
|
+
--json`, the gem reads the summary line that binary prints instead:
|
|
111
|
+
`orientation` and `rotated?` are still right, `removed` is nil, and
|
|
112
|
+
the code is `:unknown` beside the binary's own reason.
|
|
113
|
+
|
|
81
114
|
## Knobs, per process or per call
|
|
82
115
|
|
|
83
116
|
Everything the server tunes through `OXIMG_*` environment variables
|
|
@@ -106,12 +139,21 @@ Oximg.probe(Pathname("in.jpg")) # also a path
|
|
|
106
139
|
Oximg.probe(upload.tempfile) # anything with #path
|
|
107
140
|
Oximg.probe(File.open("in.jpg", "rb")) # an open File
|
|
108
141
|
Oximg.probe(StringIO.new(bytes)) # an IO: bytes in memory
|
|
109
|
-
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000
|
|
142
|
+
#=> {content_type: "image/jpeg", format: :jpeg, width: 4000, height: 3000,
|
|
143
|
+
# orientation: 1, display_width: 4000, display_height: 3000}
|
|
110
144
|
```
|
|
111
145
|
|
|
112
|
-
`probe` reads the header only and decodes no pixels.
|
|
113
|
-
the stored ones, before any EXIF rotation.
|
|
114
|
-
|
|
146
|
+
`probe` reads the header only and decodes no pixels. `width` and
|
|
147
|
+
`height` are the stored ones, before any EXIF rotation. `orientation`
|
|
148
|
+
is the EXIF value, 1..8 (1 when there is none or it is invalid, as
|
|
149
|
+
`resize` reads it), and `display_width` / `display_height` are what a
|
|
150
|
+
viewer honoring it shows: the axes exchanged for 5..8. A portrait
|
|
151
|
+
phone photo stored 4000x3000 with orientation 6, as `strip` leaves it,
|
|
152
|
+
lays out at 3000x4000. A WebP keeps its EXIF after the image data, so
|
|
153
|
+
probing one that has it reads the whole source. An `oximg` executable
|
|
154
|
+
older than this gem reports every source as orientation 1.
|
|
155
|
+
|
|
156
|
+
A String is always a path, so bytes already in memory go in as a `StringIO`. An open `File` is
|
|
115
157
|
read from its start, its position untouched, and it is the file you
|
|
116
158
|
hold that is read, even if its name has since been replaced, renamed,
|
|
117
159
|
or was relative to a directory the process has left. A closed or
|
|
@@ -161,8 +203,8 @@ Oximg.in_process_probe? #=> true on a platform gem
|
|
|
161
203
|
|
|
162
204
|
```ruby
|
|
163
205
|
Oximg.available? #=> true
|
|
164
|
-
Oximg.executable #=> "/…/gems/oximg-0.
|
|
165
|
-
Oximg.version #=> "0.
|
|
206
|
+
Oximg.executable #=> "/…/gems/oximg-0.20.0-arm64-darwin/exe/oximg"
|
|
207
|
+
Oximg.version #=> "0.20.0" # the binary's version, not the gem's
|
|
166
208
|
```
|
|
167
209
|
|
|
168
210
|
`resize` shells out to that binary — one process per image, which is
|
data/exe/oximg
CHANGED
|
Binary file
|
data/lib/oximg/binary.rb
CHANGED
|
@@ -76,11 +76,23 @@ module Oximg
|
|
|
76
76
|
[out, err]
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# True once the executable refused `strip --json` (an oximg older
|
|
80
|
+
# than 0.20): Oximg.strip then runs without it and reads the
|
|
81
|
+
# summary line that binary prints. Memoized per executable, like
|
|
82
|
+
# the path, so a process
|
|
83
|
+
# pays the refused spawn once.
|
|
84
|
+
attr_accessor :legacy_strip
|
|
85
|
+
|
|
86
|
+
def legacy_strip?
|
|
87
|
+
@legacy_strip == true
|
|
88
|
+
end
|
|
89
|
+
|
|
79
90
|
# Resets the memoized lookup. For tests, and for a process that
|
|
80
91
|
# changes OXIMG_BIN after boot.
|
|
81
92
|
def reset!
|
|
82
93
|
@path = nil
|
|
83
94
|
@version = nil
|
|
95
|
+
@legacy_strip = nil
|
|
84
96
|
end
|
|
85
97
|
|
|
86
98
|
private
|
|
Binary file
|
data/lib/oximg/native.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Oximg
|
|
|
29
29
|
BUNDLED = File.expand_path("native/#{LIBRARY}", __dir__)
|
|
30
30
|
|
|
31
31
|
# The library's oximg_probe_abi_version this code was written for.
|
|
32
|
-
ABI_VERSION =
|
|
32
|
+
ABI_VERSION = 2
|
|
33
33
|
|
|
34
34
|
# Statuses (ext/src/lib.rs).
|
|
35
35
|
OK = 0
|
|
@@ -110,7 +110,8 @@ module Oximg
|
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
def call
|
|
113
|
-
|
|
113
|
+
# Stored width, stored height, orientation (ext/src/lib.rs).
|
|
114
|
+
dims = FFI::MemoryPointer.new(:uint64, 3)
|
|
114
115
|
msg = FFI::MemoryPointer.new(:char, MESSAGE_BYTES)
|
|
115
116
|
status = yield(dims, msg)
|
|
116
117
|
return if status == READ
|
|
@@ -118,8 +119,8 @@ module Oximg
|
|
|
118
119
|
text = msg.read_string.force_encoding(Encoding::UTF_8)
|
|
119
120
|
raise ProcessingError.new(text, 1) unless status == OK
|
|
120
121
|
|
|
121
|
-
width, height = dims.read_array_of_uint64(
|
|
122
|
-
|
|
122
|
+
width, height, orientation = dims.read_array_of_uint64(3)
|
|
123
|
+
Header.build(text, width, height, orientation)
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
# nil when there is no library to use. Memoized either way, so a
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Oximg
|
|
4
|
+
# What Oximg.strip did: where it wrote, and whether the output
|
|
5
|
+
# displays upright. Built from <tt>oximg strip --json</tt>, so it is
|
|
6
|
+
# the binary's own account of the run, not a second read of the file.
|
|
7
|
+
#
|
|
8
|
+
# It stands in for the destination path Oximg.strip returned before
|
|
9
|
+
# (#176): #to_s, #to_path and #to_str are the path, and it compares
|
|
10
|
+
# equal to that path as a String, so
|
|
11
|
+
# <tt>File.size(Oximg.strip(...))</tt>, <tt>File.join</tt> and
|
|
12
|
+
# <tt>assert_equal out, Oximg.strip(...)</tt> keep working.
|
|
13
|
+
#
|
|
14
|
+
# result = Oximg.strip(upload, clean, auto_rotate: true)
|
|
15
|
+
# result.rotated? #=> false
|
|
16
|
+
# result.orientation #=> 6, the tag the output keeps
|
|
17
|
+
# result.not_rotated.code #=> :unaligned_edges
|
|
18
|
+
# result.not_rotated.message #=> "1601x1203 does not divide into ..."
|
|
19
|
+
class Stripped
|
|
20
|
+
# Why <tt>auto_rotate: true</tt> left a rotated source tagged.
|
|
21
|
+
# +code+ is the contract, a Symbol from NOT_ROTATED_CODES (or
|
|
22
|
+
# :unknown, below); +message+ is for humans and may change.
|
|
23
|
+
NotRotated = Data.define(:code, :message)
|
|
24
|
+
|
|
25
|
+
# The binary's codes (docs/features/paths.md). A code missing here
|
|
26
|
+
# still comes through, as its Symbol: a newer binary may add one.
|
|
27
|
+
NOT_ROTATED_CODES = %i[unaligned_edges corrupt over_limit not_jpeg].freeze
|
|
28
|
+
|
|
29
|
+
# The destination, absolute.
|
|
30
|
+
attr_reader :path
|
|
31
|
+
# The output's content type ("image/jpeg", ...).
|
|
32
|
+
attr_reader :content_type
|
|
33
|
+
# Output size in bytes.
|
|
34
|
+
attr_reader :bytes
|
|
35
|
+
# What was dropped: <tt>[{name: "APP1 Exif", count: 1, bytes: 1204}, ...]</tt>;
|
|
36
|
+
# nil from an oximg older than 0.20.
|
|
37
|
+
attr_reader :removed
|
|
38
|
+
# The orientation tag the output keeps, 2..8, or nil when it
|
|
39
|
+
# displays as stored (upright, untagged, or rotated upright).
|
|
40
|
+
attr_reader :orientation
|
|
41
|
+
# The orientation turned into the pixels, 2..8, or nil.
|
|
42
|
+
attr_reader :rotated
|
|
43
|
+
# A NotRotated, or nil unless auto_rotate left a rotated source
|
|
44
|
+
# tagged. From an oximg older than 0.20, which has no --json and
|
|
45
|
+
# gives the reason only as a sentence, the code is :unknown.
|
|
46
|
+
attr_reader :not_rotated
|
|
47
|
+
|
|
48
|
+
# From the hash <tt>oximg strip --json</tt> prints.
|
|
49
|
+
def self.from_json(path, report)
|
|
50
|
+
why = report["not_rotated"]
|
|
51
|
+
new(
|
|
52
|
+
path: path,
|
|
53
|
+
content_type: report["content_type"],
|
|
54
|
+
bytes: report["bytes"],
|
|
55
|
+
removed: report["removed"]&.map { |r| {name: r["name"], count: r["count"], bytes: r["bytes"]} },
|
|
56
|
+
orientation: report["orientation"],
|
|
57
|
+
rotated: report["rotated"],
|
|
58
|
+
not_rotated: why && NotRotated.new(code: why["code"].to_sym, message: why["message"])
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def initialize(path:, bytes:, content_type:, orientation:, rotated:, not_rotated:, removed: nil)
|
|
63
|
+
@path = path
|
|
64
|
+
@content_type = content_type
|
|
65
|
+
@bytes = bytes
|
|
66
|
+
@removed = removed
|
|
67
|
+
@orientation = orientation
|
|
68
|
+
@rotated = rotated
|
|
69
|
+
@not_rotated = not_rotated
|
|
70
|
+
freeze
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# True when the pixels were turned upright (auto_rotate).
|
|
74
|
+
def rotated?
|
|
75
|
+
!@rotated.nil?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# True when the output displays as stored: no orientation tag left,
|
|
79
|
+
# so its stored width and height are the displayed ones.
|
|
80
|
+
def upright?
|
|
81
|
+
@orientation.nil?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def to_s
|
|
85
|
+
@path
|
|
86
|
+
end
|
|
87
|
+
alias_method :to_path, :to_s
|
|
88
|
+
alias_method :to_str, :to_s
|
|
89
|
+
|
|
90
|
+
# Equal to another result for the same run, or to the destination
|
|
91
|
+
# path as a String (String#== defers here for anything with to_str).
|
|
92
|
+
def ==(other)
|
|
93
|
+
case other
|
|
94
|
+
when Stripped then to_h == other.to_h
|
|
95
|
+
when String then @path == other
|
|
96
|
+
else false
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_h
|
|
101
|
+
{path: @path, content_type: @content_type, bytes: @bytes, removed: @removed,
|
|
102
|
+
orientation: @orientation, rotated: @rotated, not_rotated: @not_rotated&.to_h}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def inspect
|
|
106
|
+
"#<#{self.class.name} #{@path} #{to_h.except(:path).compact.inspect}>"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
data/lib/oximg/version.rb
CHANGED
data/lib/oximg.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fcntl"
|
|
4
|
+
require "json"
|
|
4
5
|
require "tempfile"
|
|
5
6
|
require_relative "oximg/version"
|
|
6
7
|
require_relative "oximg/binary"
|
|
7
8
|
require_relative "oximg/native"
|
|
9
|
+
require_relative "oximg/stripped"
|
|
8
10
|
|
|
9
11
|
# Ruby bindings for oximg (https://github.com/oximg/oximg):
|
|
10
12
|
# high-performance image compression and resizing, in-process work for a
|
|
@@ -102,8 +104,12 @@ module Oximg
|
|
|
102
104
|
# The line `oximg probe` prints (src/cli.rs):
|
|
103
105
|
#
|
|
104
106
|
# in.gif: image/gif 120x90 (10800 stored pixels), 3 frames, 1500ms, looping forever
|
|
107
|
+
# in.jpg: image/jpeg 3000x2000 (6000000 stored pixels), orientation 6 (displayed 2000x3000)
|
|
105
108
|
#
|
|
106
|
-
# The three fields the gem reports are pinned exactly
|
|
109
|
+
# The three fields the gem reports are pinned exactly, and so is the
|
|
110
|
+
# orientation, which a non-upright source prints first after the
|
|
111
|
+
# comma (its absence means 1; the displayed size is derived here from
|
|
112
|
+
# it, not read). Everything after
|
|
107
113
|
# the comma — the animation summary, printed for an animated source
|
|
108
114
|
# only — is information the gem does not expose, and is deliberately
|
|
109
115
|
# left unparsed rather than matched: a pattern that stopped at
|
|
@@ -113,7 +119,25 @@ module Oximg
|
|
|
113
119
|
# a field missing, a continuation without the comma, or anything
|
|
114
120
|
# printed before or after it. stderr is captured separately, so a
|
|
115
121
|
# warning never lands here.
|
|
116
|
-
PROBE_LINE = /\A[^\n]*: (\S+) (\d+)x(\d+) \(\d+ stored pixels\)(?:,[^\n]*)?\n?\z/
|
|
122
|
+
PROBE_LINE = /\A[^\n]*: (\S+) (\d+)x(\d+) \(\d+ stored pixels\)(?:, orientation ([1-8]) \(displayed \d+x\d+\))?(?:,[^\n]*)?\n?\z/
|
|
123
|
+
|
|
124
|
+
# The hash probe returns, built in one place for both of its paths
|
|
125
|
+
# (the in-process library and the spawned executable).
|
|
126
|
+
module Header # :nodoc:
|
|
127
|
+
def self.build(content_type, width, height, orientation)
|
|
128
|
+
# Orientations 5-8 exchange the axes on display.
|
|
129
|
+
swapped = orientation >= 5
|
|
130
|
+
{
|
|
131
|
+
content_type: content_type,
|
|
132
|
+
format: CONTENT_TYPE_FORMATS[content_type],
|
|
133
|
+
width: width,
|
|
134
|
+
height: height,
|
|
135
|
+
orientation: orientation,
|
|
136
|
+
display_width: swapped ? height : width,
|
|
137
|
+
display_height: swapped ? width : height
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
117
141
|
|
|
118
142
|
class << self
|
|
119
143
|
# Absolute path to the executable in use; raises
|
|
@@ -176,17 +200,49 @@ module Oximg
|
|
|
176
200
|
# a rotated photo keeps only its EXIF orientation, so it still
|
|
177
201
|
# displays upright. +icc:+ is :keep or :strip (default: the
|
|
178
202
|
# binary's OXIMG_ICC, else keep); dropping a CMYK JPEG's profile is
|
|
179
|
-
# refused with a ProcessingError.
|
|
203
|
+
# refused with a ProcessingError.
|
|
204
|
+
#
|
|
205
|
+
# Returns an Oximg::Stripped: the binary's own account of the run
|
|
206
|
+
# (orientation kept, rotated?, not_rotated with a stable code). It
|
|
207
|
+
# stands in for the destination path this returned before:
|
|
208
|
+
# #to_path, #to_str and == against a String are the path.
|
|
180
209
|
#
|
|
181
210
|
# This is what <tt>saver(strip: true)</tt> / <tt>-strip</tt> did
|
|
182
211
|
# under libvips or ImageMagick, minus the re-encode.
|
|
183
212
|
#
|
|
213
|
+
# <tt>auto_rotate: true</tt> turns a rotated JPEG upright instead,
|
|
214
|
+
# still without re-encoding: the compressed 8x8 blocks are moved
|
|
215
|
+
# (what <tt>jpegtran -perfect</tt> does), so its stored and displayed
|
|
216
|
+
# dimensions agree for every reader and no orientation tag is left.
|
|
217
|
+
# That is only exact when the edges it moves are whole 8 or 16 px
|
|
218
|
+
# blocks; otherwise, and for PNG and WebP, the orientation is kept
|
|
219
|
+
# as without it, and the result says why:
|
|
220
|
+
#
|
|
221
|
+
# result = Oximg.strip(upload, clean, auto_rotate: true)
|
|
222
|
+
# unless result.upright?
|
|
223
|
+
# result.not_rotated.code #=> :unaligned_edges, :corrupt, :over_limit, :not_jpeg
|
|
224
|
+
# end
|
|
225
|
+
#
|
|
184
226
|
# +env:+ is per-call knobs, as for resize.
|
|
185
|
-
def strip(source, destination, icc: nil, env: nil)
|
|
227
|
+
def strip(source, destination, icc: nil, auto_rotate: nil, env: nil)
|
|
186
228
|
output = expand(destination, "destination")
|
|
187
|
-
argv = strip_argv(source, output, icc: icc)
|
|
188
|
-
|
|
189
|
-
|
|
229
|
+
argv = strip_argv(source, output, icc: icc, auto_rotate: auto_rotate)
|
|
230
|
+
child_env = knob_env(env)
|
|
231
|
+
unless Binary.legacy_strip?
|
|
232
|
+
begin
|
|
233
|
+
out, = Binary.run(*argv, "--json", env: child_env)
|
|
234
|
+
return Stripped.from_json(output, JSON.parse(out))
|
|
235
|
+
rescue ProcessingError => e
|
|
236
|
+
# An oximg older than 0.20 refuses the flag at parse time,
|
|
237
|
+
# before it reads or writes anything: remember that, and run
|
|
238
|
+
# the strip it does know.
|
|
239
|
+
raise unless e.status == 2 && e.message.include?('unknown option "--json"')
|
|
240
|
+
|
|
241
|
+
Binary.legacy_strip = true
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
_, err = Binary.run(*argv, env: child_env)
|
|
245
|
+
legacy_stripped(output, err)
|
|
190
246
|
end
|
|
191
247
|
|
|
192
248
|
# The child environment an +env:+ turns into: String names and
|
|
@@ -211,17 +267,24 @@ module Oximg
|
|
|
211
267
|
end
|
|
212
268
|
end
|
|
213
269
|
|
|
214
|
-
# The argv `strip` would run, minus the executable
|
|
215
|
-
# same reason as
|
|
216
|
-
|
|
270
|
+
# The argv `strip` would run, minus the executable and the --json it
|
|
271
|
+
# adds to read the outcome; public for the same reason as
|
|
272
|
+
# resize_argv.
|
|
273
|
+
def strip_argv(source, destination, icc: nil, auto_rotate: nil)
|
|
217
274
|
argv = ["strip", expand(source, "source"), expand(destination, "destination")]
|
|
218
275
|
argv.push("--icc", token(icc, ICC_MODES, "icc")) unless icc.nil?
|
|
276
|
+
# Off is strip's own default, so false adds nothing.
|
|
277
|
+
argv.push("--auto-rotate") if !auto_rotate.nil? && boolean(auto_rotate, "auto_rotate")
|
|
219
278
|
argv
|
|
220
279
|
end
|
|
221
280
|
|
|
222
281
|
# Header-only inspection — no pixels are decoded. Returns
|
|
223
|
-
# <tt>{content_type:, format:, width:, height
|
|
224
|
-
#
|
|
282
|
+
# <tt>{content_type:, format:, width:, height:, orientation:,
|
|
283
|
+
# display_width:, display_height:}</tt>. +width+ and +height+ are the
|
|
284
|
+
# stored ones; +orientation+ is the EXIF value, 1..8 (1 when there is
|
|
285
|
+
# none: the rule resize's auto-rotate applies and strip keeps), and
|
|
286
|
+
# the display size is what a viewer honoring it shows, with the axes
|
|
287
|
+
# exchanged for 5..8.
|
|
225
288
|
#
|
|
226
289
|
# +source+ is a path — a String, a Pathname, or anything with #path
|
|
227
290
|
# such as a Tempfile — an open File, or a stream read from its
|
|
@@ -363,12 +426,31 @@ module Oximg
|
|
|
363
426
|
match = output.match(PROBE_LINE)
|
|
364
427
|
raise ProcessingError, "unparsable probe output: #{output.inspect}" unless match
|
|
365
428
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
429
|
+
Header.build(match[1], Integer(match[2]), Integer(match[3]), match[4] ? Integer(match[4]) : 1)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# The summary every oximg since 0.18 ends a strip with, after
|
|
433
|
+
# "oximg: wrote <out> " (src/cli.rs). Those releases are frozen, so
|
|
434
|
+
# their wording is a fixed format here, though it is not a contract
|
|
435
|
+
# for later ones (they have --json). Removed block names are JPEG
|
|
436
|
+
# segment names or 4-byte chunk types, so the tail is unambiguous.
|
|
437
|
+
LEGACY_STRIP_SUMMARY = /\A\((\d+) bytes, ([^;\s]+); (.*?)(?:; kept orientation ([2-8])(?:, not rotated: (.*))?|; rotated orientation ([2-8]) upright)?\)\z/m
|
|
438
|
+
|
|
439
|
+
# The result from an oximg without --json, read from that summary
|
|
440
|
+
# rather than by probing: probe stops looking for the orientation
|
|
441
|
+
# after 1 MiB of metadata and needs a header libjpeg reads, where
|
|
442
|
+
# strip does neither. The reason a rotation was not made has no
|
|
443
|
+
# code before 0.20, so it is :unknown, with the binary's sentence.
|
|
444
|
+
def legacy_stripped(output, stderr)
|
|
445
|
+
prefix = "oximg: wrote #{output} "
|
|
446
|
+
line = stderr.lines.map(&:chomp).reverse.find { |l| l.start_with?(prefix) }
|
|
447
|
+
match = line && line.delete_prefix(prefix).match(LEGACY_STRIP_SUMMARY)
|
|
448
|
+
raise ProcessingError, "unparsable strip summary: #{stderr.strip.inspect}" unless match
|
|
449
|
+
|
|
450
|
+
kept = match[4] && Integer(match[4])
|
|
451
|
+
why = match[5] && Stripped::NotRotated.new(code: :unknown, message: match[5])
|
|
452
|
+
Stripped.new(path: output, bytes: Integer(match[1]), content_type: match[2],
|
|
453
|
+
orientation: kept, rotated: match[6] && Integer(match[6]), not_rotated: why)
|
|
372
454
|
end
|
|
373
455
|
|
|
374
456
|
# Absolute paths throughout: a relative name beginning with "-"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: oximg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.20.0
|
|
5
5
|
platform: aarch64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- oximg contributors
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- lib/oximg/binary.rb
|
|
38
38
|
- lib/oximg/native.rb
|
|
39
39
|
- lib/oximg/native/liboximg_probe.so
|
|
40
|
+
- lib/oximg/stripped.rb
|
|
40
41
|
- lib/oximg/version.rb
|
|
41
42
|
homepage: https://github.com/oximg/oximg
|
|
42
43
|
licenses:
|