Skip to content

Commit 27e4a29

Browse files
committed
Add support for reading exif tags from IO objects
- Updated `README.md` to include instructions for reading metadata from IO objects. - Modified `lib/exiftool.rb` to handle IO objects as input, allowing ExifTool to infer file type from content. - Utilized `Open3.popen3` for executing shell commands, ensuring proper process management and error handling. - Added a test case in `test/exiftool_test.rb`.
1 parent 330f62f commit 27e4a29

File tree

3 files changed

+72
-8
lines changed

3 files changed

+72
-8
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@ e.files_with_results
8383
# => ["path/to/iPhone 4S.jpg", "path/to/Droid X.jpg", …
8484
```
8585

86+
### Reading from IO
87+
88+
You can pass an IO object to read metadata from standard input. ExifTool will infer the file type from content:
89+
90+
```ruby
91+
io = File.open('test/IMG_2452.jpg', 'rb')
92+
e = Exiftool.new(io)
93+
e[:make] # => "Canon"
94+
```
95+
8696
### Dates without timezones
8797

8898
It seems that most exif dates don't include timezone offsets, without which forces us to assume the

lib/exiftool.rb

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'json'
44
require 'shellwords'
5+
require 'open3'
56
require 'exiftool/result'
67
require 'forwardable'
78
require 'pathname'
@@ -30,7 +31,20 @@ def self.exiftool_installed?
3031

3132
# This is a string, not a float, to handle versions like "9.40" properly.
3233
def self.exiftool_version
33-
@exiftool_version ||= `#{command} -ver 2> /dev/null`.chomp
34+
return @exiftool_version if defined?(@exiftool_version) && @exiftool_version
35+
36+
stdout_str = ''
37+
begin
38+
Open3.popen3(command, '-ver') do |_stdin, stdout, _stderr, wait_thr|
39+
stdout_str = stdout.read.to_s.chomp
40+
# Ensure the process is reaped
41+
wait_thr.value
42+
end
43+
rescue Errno::ENOENT
44+
stdout_str = ''
45+
end
46+
47+
@exiftool_version = stdout_str
3448
end
3549

3650
def self.expand_path(filename)
@@ -47,16 +61,45 @@ def self.expand_path(filename)
4761

4862
def initialize(filenames, exiftool_opts = '')
4963
@file2result = {}
64+
io_input = nil
65+
if filenames.is_a?(IO)
66+
io_input = filenames
67+
filenames = ['-']
68+
end
69+
5070
filenames = [filenames] if filenames.is_a?(String) || filenames.is_a?(Pathname)
5171
return if filenames.empty?
5272

53-
escaped_filenames = filenames.map do |f|
54-
Shellwords.escape(self.class.expand_path(f.to_s))
55-
end.join(' ')
56-
# I'd like to use -dateformat, but it doesn't support timezone offsets properly,
57-
# nor sub-second timestamps.
58-
cmd = "#{self.class.command} #{exiftool_opts} -j -coordFormat \"%.8f\" #{escaped_filenames} 2> /dev/null"
59-
json = `#{cmd}`.chomp
73+
expanded_filenames = filenames.map do |f|
74+
f == '-' ? '-' : self.class.expand_path(f.to_s)
75+
end
76+
args = [
77+
self.class.command,
78+
*Shellwords.split(exiftool_opts),
79+
'-j',
80+
'-coordFormat', '%.8f',
81+
*expanded_filenames
82+
]
83+
84+
json = ''
85+
begin
86+
Open3.popen3(*args) do |stdin, stdout, _stderr, wait_thr|
87+
if io_input
88+
# Reading first 64KB.
89+
# It is enough to parse exif tags.
90+
# https://en.wikipedia.org/wiki/Exif#Technical_2
91+
while (chunk = io_input.read(1 << 16))
92+
stdin.write(chunk)
93+
end
94+
stdin.close
95+
end
96+
json = stdout.read.to_s.chomp
97+
wait_thr.value
98+
end
99+
rescue Errno::ENOENT
100+
json = ''
101+
end
102+
60103
raise ExiftoolNotInstalled if json == ''
61104

62105
JSON.parse(json).each do |raw|

test/exiftool_test.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@
8080
validate_result(e, 'test/utf8.jpg')
8181
end
8282

83+
it 'supports an IO object as a constructor arg' do
84+
File.open('test/IMG_2452.jpg', 'rb') do |io|
85+
e = Exiftool.new(io)
86+
_(e.errors?).must_be_false
87+
h = e.to_hash
88+
_(h[:file_type]).must_equal 'JPEG'
89+
_(h[:mime_type]).must_equal 'image/jpeg'
90+
_(h[:make]).must_equal 'Canon'
91+
end
92+
end
93+
8394
describe 'single-get' do
8495
it 'responds with known correct responses' do
8596
Dir['test/*.jpg'].each do |filename|

0 commit comments

Comments
 (0)