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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 64KB restriction only applies to JPEG format? Any idea on similar restriction in regards to other image and video formats?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did a little bit more digging and found some interesting info related to that 64KB limit here. https://exiftool.org/commentary.html#JPEG
I'm wondering if limiting io input to first 64KB would be advisable.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there is some misconception in the comment. I sends the whole file in 64KB chunks.

LLM suggests a better way of doing it using:

IO.copy_stream(io_input, stdin)

Quote:

What IO.copy_stream does

  • Streams efficiently from one IO to another

  • Uses optimal chunk size internally

  • Stops automatically when either:

    • io_input reaches EOF, or
    • stdin (exiftool) closes early

When exiftool closes stdin early (because it has enough data),
IO.copy_stream simply stops and returns — no broken pipe errors.

What do you think?

# 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