class MiniExiftool
Simple OO access to the ExifTool command-line application.
Constants
- VERSION
Attributes
Public Class Methods
Returns the command name of the called ExifTool application.
# File lib/mini_exiftool.rb, line 313 def self.command @@cmd end
Setting the command name of the called ExifTool application.
# File lib/mini_exiftool.rb, line 318 def self.command= cmd @@cmd = cmd end
# File lib/mini_exiftool.rb, line 62 def self.encoding_opt enc_type (enc_type.to_s + '_encoding').to_sym end
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
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
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
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
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 isfalse
-
:composite
for including composite tags while loading, default istrue
-
:ignore_minor_errors
ignore minor errors (See -m-option of the exiftool command-line application, default isfalse
) -
:coord_format
set format for GPS coordinates (See -c-option of the exiftool command-line application, default isnil
that means exiftool standard) -
:fast
useful when reading JPEGs over a slow network connection (See -fast-option of the exiftool command-line application, default isfalse
) -
:fast2
useful when reading JPEGs over a slow network connection (See -fast2-option of the exiftool command-line application, default isfalse
) -
:replace_invalid_chars
replace string for invalid UTF-8 characters orfalse
if no replacing should be done, default isfalse
-
:timestamps
generating DateTime objects instead of Time objects if set toDateTime
, default isTime
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 isnil
: 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
Returns the options hash.
# File lib/mini_exiftool.rb, line 323 def self.opts @@opts end
# 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
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
# 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
# File lib/mini_exiftool.rb, line 376 def self.pstore_dir= dir @@pstore_dir = dir end
# File lib/mini_exiftool.rb, line 360 def self.unify tag tag.to_s.gsub(/[-_]/,'').downcase end
Private Class Methods
# 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
# 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
Returns the value of a tag.
# File lib/mini_exiftool.rb, line 172 def [] tag @changed_values[tag] || @values[tag] end
Set the value of a tag.
# File lib/mini_exiftool.rb, line 177 def []= tag, val @changed_values[tag] = val end
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
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 the tags of an already read file.
# File lib/mini_exiftool.rb, line 167 def reload load @filename end
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 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
# 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
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
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
# 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
# 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
# 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
# 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
# File lib/mini_exiftool.rb, line 539 def escape val '"' << val.to_s.gsub(/([\\"`])/, "\\\\\\1") << '"' end
# 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
# 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
# File lib/mini_exiftool.rb, line 435 def parse_output adapt_encoding set_values JSON.parse(@output).first end
# 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
# 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
# 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
# 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