Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ e.files_with_results
# => ["path/to/iPhone 4S.jpg", "path/to/Droid X.jpg", …
```

### Reading from IO

You can pass an IO object to read metadata from standard input. ExifTool will infer the file type from content:

```ruby
io = File.open('test/IMG_2452.jpg', 'rb')
e = Exiftool.new(io)
e[:make] # => "Canon"
```

### Dates without timezones

It seems that most exif dates don't include timezone offsets, without which forces us to assume the
Expand Down
59 changes: 51 additions & 8 deletions lib/exiftool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'json'
require 'shellwords'
require 'open3'
require 'exiftool/result'
require 'forwardable'
require 'pathname'
Expand Down Expand Up @@ -30,7 +31,20 @@ def self.exiftool_installed?

# This is a string, not a float, to handle versions like "9.40" properly.
def self.exiftool_version
@exiftool_version ||= `#{command} -ver 2> /dev/null`.chomp
return @exiftool_version if defined?(@exiftool_version) && @exiftool_version

stdout_str = ''
begin
Open3.popen3(command, '-ver') do |_stdin, stdout, _stderr, wait_thr|
stdout_str = stdout.read.to_s.chomp
# Ensure the process is reaped
wait_thr.value
end
rescue Errno::ENOENT
stdout_str = ''
end

@exiftool_version = stdout_str
end

def self.expand_path(filename)
Expand All @@ -47,16 +61,45 @@ def self.expand_path(filename)

def initialize(filenames, exiftool_opts = '')
@file2result = {}
io_input = nil
if filenames.is_a?(IO)
io_input = filenames
filenames = ['-']
end

filenames = [filenames] if filenames.is_a?(String) || filenames.is_a?(Pathname)
return if filenames.empty?

escaped_filenames = filenames.map do |f|
Shellwords.escape(self.class.expand_path(f.to_s))
end.join(' ')
# I'd like to use -dateformat, but it doesn't support timezone offsets properly,
# nor sub-second timestamps.
cmd = "#{self.class.command} #{exiftool_opts} -j -coordFormat \"%.8f\" #{escaped_filenames} 2> /dev/null"
json = `#{cmd}`.chomp
expanded_filenames = filenames.map do |f|
f == '-' ? '-' : self.class.expand_path(f.to_s)
end
args = [
self.class.command,
*Shellwords.split(exiftool_opts),
'-j',
'-coordFormat', '%.8f',
*expanded_filenames
]

json = ''
begin
Open3.popen3(*args) do |stdin, stdout, _stderr, wait_thr|
if io_input
# Reading first 64KB.
# It is enough to parse exif tags.
# https://en.wikipedia.org/wiki/Exif#Technical_2
while (chunk = io_input.read(1 << 16))
stdin.write(chunk)
end
stdin.close
end
json = stdout.read.to_s.chomp
wait_thr.value
end
rescue Errno::ENOENT
json = ''
end

raise ExiftoolNotInstalled if json == ''

JSON.parse(json).each do |raw|
Expand Down
11 changes: 11 additions & 0 deletions test/exiftool_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@
validate_result(e, 'test/utf8.jpg')
end

it 'supports an IO object as a constructor arg' do
File.open('test/IMG_2452.jpg', 'rb') do |io|
e = Exiftool.new(io)
_(e.errors?).must_be_false
h = e.to_hash
_(h[:file_type]).must_equal 'JPEG'
_(h[:mime_type]).must_equal 'image/jpeg'
_(h[:make]).must_equal 'Canon'
end
end

describe 'single-get' do
it 'responds with known correct responses' do
Dir['test/*.jpg'].each do |filename|
Expand Down
Loading