class MiniExiftool

Simple OO access to the ExifTool command-line application.

Constants

VERSION

Attributes

errors[R]
filename[R]
io[R]

Public Class Methods

all_tags() click to toggle source

Returns a set of all known tags of ExifTool.

# File lib/mini_exiftool.rb, line 328
def self.all_tags
  unless defined? @@all_tags
    @@all_tags = pstore_get :all_tags
  end
  @@all_tags
end
command() click to toggle source

Returns the command name of the called ExifTool application.

# File lib/mini_exiftool.rb, line 313
def self.command
  @@cmd
end
command=(cmd) click to toggle source

Setting the command name of the called ExifTool application.

# File lib/mini_exiftool.rb, line 318
def self.command= cmd
  @@cmd = cmd
end
encoding_opt(enc_type) click to toggle source
# File lib/mini_exiftool.rb, line 62
def self.encoding_opt enc_type
  (enc_type.to_s + '_encoding').to_sym
end
exiftool_version() click to toggle source

Returns the version of the ExifTool command-line application.

# File lib/mini_exiftool.rb, line 352
def self.exiftool_version
  Open3.popen3 "#{MiniExiftool.command} -ver" do |_inp, out, _err, _thr|
    out.read.chomp!
  end
rescue SystemCallError
  raise MiniExiftool::Error.new("Command '#{MiniExiftool.command}' not found")
end
from_hash(hash, opts={}) click to toggle source

Create a MiniExiftool instance from a hash. Default value conversions will be applied if neccesary.

# File lib/mini_exiftool.rb, line 292
def self.from_hash hash, opts={}
  instance = MiniExiftool.new nil, opts
  instance.initialize_from_hash hash
  instance
end
from_json(json, opts={}) click to toggle source

Create a MiniExiftool instance from JSON data. Default value conversions will be applied if neccesary.

# File lib/mini_exiftool.rb, line 300
def self.from_json json, opts={}
  instance = MiniExiftool.new nil, opts
  instance.initialize_from_json json
  instance
end
from_yaml(yaml, opts={}) click to toggle source

Create a MiniExiftool instance from YAML data created with MiniExiftool#to_yaml

# File lib/mini_exiftool.rb, line 308
def self.from_yaml yaml, opts={}
  MiniExiftool.from_hash YAML.unsafe_load(yaml), opts
end
new(filename_or_io=nil, opts={}) click to toggle source

filename_or_io The kind of the parameter is determined via duck typing: if the argument responds to to_str it is interpreted as filename, if it responds to read it is interpreted es IO instance.

ATTENTION: If using an IO instance writing of meta data is not supported!

opts support at the moment

  • :numerical for numerical values, default is false

  • :composite for including composite tags while loading, default is true

  • :ignore_minor_errors ignore minor errors (See -m-option of the exiftool command-line application, default is false)

  • :coord_format set format for GPS coordinates (See -c-option of the exiftool command-line application, default is nil that means exiftool standard)

  • :fast useful when reading JPEGs over a slow network connection (See -fast-option of the exiftool command-line application, default is false)

  • :fast2 useful when reading JPEGs over a slow network connection (See -fast2-option of the exiftool command-line application, default is false)

  • :replace_invalid_chars replace string for invalid UTF-8 characters or false if no replacing should be done, default is false

  • :timestamps generating DateTime objects instead of Time objects if set to DateTime, default is Time

    ATTENTION: Time objects are created using Time.local therefore they use your local timezone, DateTime objects instead are created without timezone!

  • :exif_encoding, :iptc_encoding, :xmp_encoding, :png_encoding, :id3_encoding, :pdf_encoding, :photoshop_encoding, :quicktime_encoding, :aiff_encoding, :mie_encoding, :vorbis_encoding to set this specific encoding (see -charset option of the exiftool command-line application, default is nil: no encoding specified)

# File lib/mini_exiftool.rb, line 106
def initialize filename_or_io=nil, opts={}
  @opts = @@opts.merge opts
  if @opts[:convert_encoding]
    warn 'Option :convert_encoding is not longer supported!'
    warn 'Please use the String#encod* methods.'
  end
  @filename = nil
  @io = nil
  @values = TagHash.new
  @changed_values = TagHash.new
  @errors = TagHash.new
  load filename_or_io unless filename_or_io.nil?
end
opts() click to toggle source

Returns the options hash.

# File lib/mini_exiftool.rb, line 323
def self.opts
  @@opts
end
opts_accessor(*attrs) click to toggle source
# File lib/mini_exiftool.rb, line 44
def self.opts_accessor *attrs
  attrs.each do |a|
    define_method a do
      @opts[a]
    end
    define_method "#{a}=" do |val|
      @opts[a] = val
    end
  end
end
original_tag(tag) click to toggle source

Returns the original ExifTool name of the given tag

# File lib/mini_exiftool.rb, line 344
def self.original_tag tag
  unless defined? @@all_tags_map
    @@all_tags_map = pstore_get :all_tags_map
  end
  @@all_tags_map[tag]
end
pstore_dir() click to toggle source
# File lib/mini_exiftool.rb, line 366
def self.pstore_dir
  unless defined? @@pstore_dir
    # This will hopefully work on *NIX and Windows systems
    home = ENV['HOME'] || ENV['HOMEDRIVE'] + ENV['HOMEPATH'] || ENV['USERPROFILE']
    subdir = @@running_on_windows ? '_mini_exiftool' : '.mini_exiftool'
    @@pstore_dir = File.join(home, subdir)
  end
  @@pstore_dir
end
pstore_dir=(dir) click to toggle source
# File lib/mini_exiftool.rb, line 376
def self.pstore_dir= dir
  @@pstore_dir = dir
end
unify(tag) click to toggle source
# File lib/mini_exiftool.rb, line 360
def self.unify tag
  tag.to_s.gsub(/[-_]/,'').downcase
end
writable_tags() click to toggle source

Returns a set of all possible writable tags of ExifTool.

# File lib/mini_exiftool.rb, line 336
def self.writable_tags
  unless defined? @@writable_tags
    @@writable_tags = pstore_get :writable_tags
  end
  @@writable_tags
end

Private Class Methods

determine_tags(arg) click to toggle source
# File lib/mini_exiftool.rb, line 527
def self.determine_tags arg
  output = `#{@@cmd} -#{arg}`
  lines = output.split(/\n/)
  tags = Set.new
  lines.each do |line|
    next unless line =~ /^\s/
    tags |= line.chomp.split
  end
  tags
end
load_or_create_pstore() click to toggle source
# File lib/mini_exiftool.rb, line 510
def self.load_or_create_pstore
  FileUtils.mkdir_p(pstore_dir)
  pstore_filename = File.join(pstore_dir, 'exiftool_tags_' << exiftool_version.gsub('.', '_') << '.pstore')
  @@pstore = PStore.new(pstore_filename, _threadsafe = true)
  if !File.exist?(pstore_filename) || File.size(pstore_filename) == 0
    $stderr.puts 'Generating cache file for ExifTool tag names. This takes a few seconds but is only needed once...'
    @@pstore.transaction do |ps|
      ps[:all_tags] = all_tags = determine_tags('list')
      ps[:writable_tags] = determine_tags('listw')
      map = {}
      all_tags.each { |k| map[unify(k)] = k }
      ps[:all_tags_map] = map
    end
    $stderr.puts 'Cache file generated.'
  end
end
pstore_get(attribute) click to toggle source
# File lib/mini_exiftool.rb, line 501
def self.pstore_get attribute
  load_or_create_pstore unless defined? @@pstore
  result = nil
  @@pstore.transaction(true) do |ps|
    result = ps[attribute]
  end
  result
end

Public Instance Methods

[](tag) click to toggle source

Returns the value of a tag.

# File lib/mini_exiftool.rb, line 172
def [] tag
  @changed_values[tag] || @values[tag]
end
[]=(tag, val) click to toggle source

Set the value of a tag.

# File lib/mini_exiftool.rb, line 177
def []= tag, val
  @changed_values[tag] = val
end
changed?(tag=false) click to toggle source

Returns true if any tag value is changed or if the value of a given tag is changed.

# File lib/mini_exiftool.rb, line 183
def changed? tag=false
  if tag
    @changed_values.include? tag
  else
    !@changed_values.empty?
  end
end
changed_tags() click to toggle source

Returns an array of all changed tags.

# File lib/mini_exiftool.rb, line 209
def changed_tags
  @changed_values.keys.map { |key| MiniExiftool.original_tag(key) }
end
copy_tags_from(source_filename, tags) click to toggle source
# File lib/mini_exiftool.rb, line 260
def copy_tags_from(source_filename, tags)
  @errors.clear
  unless File.exist?(source_filename)
    raise MiniExiftool::Error.new("Source file #{source_filename} does not exist!")
  end
  params = '-q -P -overwrite_original '
  tags_params = Array(tags).map {|t| '-' << t.to_s}.join(' ')
  cmd = [@@cmd, params, '-tagsFromFile', escape(source_filename).encode(@@fs_enc), tags_params.encode('UTF-8'), escape(filename).encode(@@fs_enc)].join(' ')
  cmd.force_encoding('UTF-8')
  result = run(cmd)
  reload
  result
end
load(filename_or_io) click to toggle source

Load the tags of filename or io.

# File lib/mini_exiftool.rb, line 134
def load filename_or_io
  if filename_or_io.respond_to? :to_str # String-like
    unless filename_or_io && File.exist?(filename_or_io)
      raise MiniExiftool::Error.new("File '#{filename_or_io}' does not exist.")
    end
    if File.directory?(filename_or_io)
      raise MiniExiftool::Error.new("'#{filename_or_io}' is a directory.")
    end
    @filename = filename_or_io.to_str
  elsif filename_or_io.respond_to? :read # IO-like
    @io = filename_or_io
    @filename = '-'
  else
    raise MiniExiftool::Error.new("Could not open filename_or_io.")
  end
  @values.clear
  @changed_values.clear
  params = '-j '
  params << (@opts[:numerical] ? '-n ' : '')
  params << (@opts[:composite] ? '' : '-e ')
  params << (@opts[:coord_format] ? "-c #{escape(@opts[:coord_format])}" : '')
  params << (@opts[:fast] ? '-fast ' : '')
  params << (@opts[:fast2] ? '-fast2 ' : '')
  params << generate_encoding_params
  if run(cmd_gen(params, @filename))
    parse_output
  else
    raise MiniExiftool::Error.new(@error_text)
  end
  self
end
reload() click to toggle source

Reload the tags of an already read file.

# File lib/mini_exiftool.rb, line 167
def reload
  load @filename
end
revert(tag=nil) click to toggle source

Revert all changes or the change of a given tag.

# File lib/mini_exiftool.rb, line 192
def revert tag=nil
  if tag
    val = @changed_values.delete(tag)
    res = val != nil
  else
    res = @changed_values.size > 0
    @changed_values.clear
  end
  res
end
save() click to toggle source

Save the changes to the file.

# File lib/mini_exiftool.rb, line 214
def save
  if @io
    raise MiniExiftool::Error.new('No writing support when using an IO.')
  end
  return false if @changed_values.empty?
  @errors.clear
  temp_file = Tempfile.new('mini_exiftool')
  temp_file.close
  temp_filename = temp_file.path
  FileUtils.cp filename.encode(@@fs_enc), temp_filename
  all_ok = true
  @changed_values.each do |tag, val|
    original_tag = MiniExiftool.original_tag(tag)
    arr_val = val.kind_of?(Array) ? val : [val]
    arr_val.map! {|e| convert_before_save(e)}
    params = '-q -P -overwrite_original '
    params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '')
    params << (@opts[:ignore_minor_errors] ? '-m ' : '')
    params << generate_encoding_params
    arr_val.each do |v|
      params << %Q(-#{original_tag}=#{escape(v)} )
    end
    result = run(cmd_gen(params, temp_filename))
    unless result
      all_ok = false
      @errors[tag] = @error_text.gsub(/Nothing to do.\n\z/, '').chomp
    end
  end
  if all_ok
    FileUtils.cp temp_filename, filename.encode(@@fs_enc)
    reload
  end
  temp_file.delete
  all_ok
end
save!() click to toggle source
# File lib/mini_exiftool.rb, line 250
def save!
  unless save
    err = []
    @errors.each do |key, value|
      err << "(#{key}) #{value}"
    end
    raise MiniExiftool::Error.new("MiniExiftool couldn't save. The following errors occurred: #{err.empty? ? "None" : err.join(", ")}")
  end
end
tags() click to toggle source

Returns an array of the tags (original tag names) of the read file.

# File lib/mini_exiftool.rb, line 204
def tags
  @values.keys.map { |key| MiniExiftool.original_tag(key) }
end
to_hash() click to toggle source

Returns a hash of the original loaded values of the MiniExiftool instance.

# File lib/mini_exiftool.rb, line 276
def to_hash
  result = {}
  @values.each do |k,v|
    result[MiniExiftool.original_tag(k)] = v
  end
  result
end
to_yaml() click to toggle source

Returns a YAML representation of the original loaded values of the MiniExiftool instance.

# File lib/mini_exiftool.rb, line 286
def to_yaml
  to_hash.to_yaml
end

Private Instance Methods

adapt_encoding() click to toggle source
# File lib/mini_exiftool.rb, line 440
def adapt_encoding
  @output.force_encoding('UTF-8')
  if @opts[:replace_invalid_chars] && !@output.valid_encoding?
    @output.encode!('UTF-16le', invalid: :replace, replace: @opts[:replace_invalid_chars]).encode!('UTF-8')
  end
end
cmd_gen(arg_str='', filename) click to toggle source
# File lib/mini_exiftool.rb, line 387
def cmd_gen arg_str='', filename
  [@@cmd, arg_str.encode('UTF-8'), escape(filename.encode(@@fs_enc))].map {|s| s.force_encoding('UTF-8')}.join(' ')
end
convert_after_load(tag, value) click to toggle source
# File lib/mini_exiftool.rb, line 447
def convert_after_load tag, value
  return value unless value.kind_of?(String)
  return value unless value.valid_encoding?
  case value
  when /^\d{4}:\d\d:\d\d \d\d:\d\d:\d\d/
    s = value.sub(/^(\d+):(\d+):/, '\1-\2-')
    begin
      if @opts[:timestamps] == Time
        value = Time.parse(s)
      elsif @opts[:timestamps] == DateTime
        value = DateTime.parse(s)
      else
        raise MiniExiftool::Error.new("Value #{@opts[:timestamps]} not allowed for option timestamps.")
      end
    rescue ArgumentError, RangeError
      value = false
    end
  when /^\+\d+\.\d+$/
    value = value.to_f
  when /^0\d+$/
    # no conversion => String
  when /^-?\d+$/
    value = value.to_i
  when %r(^(\d+)/(\d+)$)
    value = Rational($1.to_i, $2.to_i) rescue value
  when /^[\d ]+$/
    # nothing => String
  end
  value
end
convert_before_save(val) click to toggle source
# File lib/mini_exiftool.rb, line 413
def convert_before_save val
  case val
  when Time
    val = val.strftime('%Y:%m:%d %H:%M:%S')
  end
  val
end
escape(val) click to toggle source
# File lib/mini_exiftool.rb, line 539
def escape val
  '"' << val.to_s.gsub(/([\\"`])/, "\\\\\\1") << '"'
end
generate_encoding_params() click to toggle source
# File lib/mini_exiftool.rb, line 548
def generate_encoding_params
  params = ''
  @@encoding_types.each do |enc_type|
    if enc_val = @opts[MiniExiftool.encoding_opt(enc_type)]
      params << "-charset #{enc_type}=#{enc_val} "
    end
  end
  params
end
method_missing(symbol, *args) click to toggle source
# File lib/mini_exiftool.rb, line 421
def method_missing symbol, *args
  tag_name = symbol.id2name
  if tag_name =~ /^(.+)=$/
    self[$1] = args.first
  else
    self[tag_name]
  end
end
parse_output() click to toggle source
# File lib/mini_exiftool.rb, line 435
def parse_output
  adapt_encoding
  set_values JSON.parse(@output).first
end
respond_to_missing?(symbol, *args) click to toggle source
Calls superclass method
# File lib/mini_exiftool.rb, line 430
def respond_to_missing? symbol, *args
  tag_name = MiniExiftool.unify(symbol.id2name)
  !!(tag_name =~ /=$/) || @values.key?(tag_name) || super
end
run(cmd) click to toggle source
# File lib/mini_exiftool.rb, line 391
def run cmd
  if $DEBUG
    $stderr.puts cmd
  end
  status = Open3.popen3(cmd) do |inp, out, err, thr|
    if @io
      begin
        IO.copy_stream @io, inp
      rescue Errno::EPIPE
        # Output closed, no problem
      rescue ::IOError => e
        raise MiniExiftool::Error.new("IO is not readable.")
      end
      inp.close
    end
    @output = out.read
    @error_text = err.read
    thr.value.exitstatus
  end
  status == 0
end
set_opts_by_heuristic() click to toggle source
# File lib/mini_exiftool.rb, line 495
def set_opts_by_heuristic
  @opts[:composite] = tags.include?('ImageSize')
  @opts[:numerical] = self.file_size.kind_of?(Integer)
  @opts[:timestamps] = self.FileModifyDate.kind_of?(DateTime) ? DateTime : Time
end
set_values(hash) click to toggle source
# File lib/mini_exiftool.rb, line 478
def set_values hash
  hash.each_pair do |tag,val|
    @values[tag] = convert_after_load(tag, val)
  end
  # Remove filename specific tags use attr_reader
  # MiniExiftool#filename instead
  # Cause: value of tag filename and attribute
  # filename have different content, the latter
  # holds the filename with full path (like the
  # sourcefile tag) and the former the basename
  # of the filename also there is no official
  # "original tag name" for sourcefile
  %w(directory filename sourcefile).each do |t|
    @values.delete(t)
  end
end