├── .yardopts ├── Gemfile ├── test ├── data │ ├── invalid_rational.json │ ├── Canon.jpg │ ├── test.jpg │ ├── INFORMATION │ ├── Bad_PreviewIFD.jpg │ ├── test_encodings.jpg │ ├── test_coordinates.jpg │ ├── test_special_dates.jpg │ ├── invalid_byte_sequence_in_utf8.json │ └── test.jpg.json ├── test_invalid_rational.rb ├── test_composite.rb ├── test_read_coordinates.rb ├── helpers_for_test.rb ├── test_pstore.rb ├── test_invalid_byte_sequence_in_utf8.rb ├── test_bad_preview_ifd.rb ├── test_from_hash.rb ├── test_encodings.rb ├── test_special.rb ├── test_read_numerical.rb ├── test_special_dates.rb ├── test_io.rb ├── test_filename_access.rb ├── test_read.rb ├── test_copy_tags_from.rb ├── test_class_methods.rb ├── test_save.rb ├── test_dumping.rb └── test_write.rb ├── .aspell.pws ├── .gitignore ├── TODO ├── regtest ├── read_all.rb └── read_all.yml ├── .travis.yml ├── examples ├── copy_icc_profile.rb ├── external_photo.rb ├── print_portraits.rb ├── shift_time.rb └── show_speedup_with_fast_option.rb ├── Rakefile ├── mini_exiftool.gemspec ├── README.rdoc ├── Tutorial.rdoc ├── Changelog ├── lib └── mini_exiftool.rb ├── COPYING └── setup.rb /.yardopts: -------------------------------------------------------------------------------- 1 | --files Tutorial.rdoc,examples/*.rb 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /test/data/invalid_rational.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "UserComment": "1/0" 3 | }] 4 | -------------------------------------------------------------------------------- /.aspell.pws: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/.aspell.pws -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.swp 3 | doc 4 | pkg 5 | .yardoc 6 | .rimrc 7 | Gemfile.lock 8 | -------------------------------------------------------------------------------- /test/data/Canon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/test/data/Canon.jpg -------------------------------------------------------------------------------- /test/data/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/test/data/test.jpg -------------------------------------------------------------------------------- /test/data/INFORMATION: -------------------------------------------------------------------------------- 1 | Following files are borrowed from the original ExifTool perl package. 2 | 3 | Canon.jpg -------------------------------------------------------------------------------- /test/data/Bad_PreviewIFD.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/test/data/Bad_PreviewIFD.jpg -------------------------------------------------------------------------------- /test/data/test_encodings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/test/data/test_encodings.jpg -------------------------------------------------------------------------------- /test/data/test_coordinates.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/test/data/test_coordinates.jpg -------------------------------------------------------------------------------- /test/data/test_special_dates.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/test/data/test_special_dates.jpg -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Tests for managing tags with pstore 2 | * Looking for a solution to dump and restore DateTime instances with YAML 3 | -------------------------------------------------------------------------------- /test/data/invalid_byte_sequence_in_utf8.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcherry/mini_exiftool/master/test/data/invalid_byte_sequence_in_utf8.json -------------------------------------------------------------------------------- /regtest/read_all.rb: -------------------------------------------------------------------------------- 1 | require 'mini_exiftool' 2 | require 'regtest' 3 | 4 | Dir['test/data/*.jpg'].sort.each do |fn| 5 | Regtest.sample 'read ' << File.basename(fn) do 6 | h = MiniExiftool.new(fn).to_hash 7 | %w(FileModifyDate FileAccessDate FileInodeChangeDate FilePermissions).each do |tag| 8 | h.delete(tag) 9 | end 10 | h 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/test_invalid_rational.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestInvalidRational < TestCase 5 | 6 | def test_rescue_from_invalid_rational 7 | mini_exiftool = MiniExiftool.from_json(File.read('test/data/invalid_rational.json')) 8 | assert_equal '1/0', mini_exiftool.user_comment 9 | rescue Exception 10 | assert false, 'Tag values of the form x/0 should not raise an Exception.' 11 | end 12 | 13 | end 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - sudo apt-get install wget 4 | - wget http://www.sno.phy.queensu.ca/~phil/exiftool/Image-ExifTool-9.76.tar.gz 5 | - tar -xzf Image-ExifTool-9.76.tar.gz 6 | - cd Image-ExifTool-9.76 7 | - perl Makefile.PL 8 | - make 9 | - sudo make install 10 | - cd .. 11 | - exiftool -ver 12 | rvm: 13 | - 2.0.0 14 | - 2.1.9 15 | - 2.2.5 16 | - 2.3.1 17 | - ruby-head 18 | notifications: 19 | email: 20 | - janfri26@gmail.com 21 | -------------------------------------------------------------------------------- /examples/copy_icc_profile.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'mini_exiftool' 3 | 4 | if ARGV.size < 2 5 | puts "usage: ruby #{__FILE__} SOURCE_FILE TARGET_FILE" 6 | exit -1 7 | end 8 | 9 | source_filename, target_filename = ARGV 10 | 11 | begin 12 | photo = MiniExiftool.new filename 13 | # The second parameter of MiniExiftool#copy_tags_from 14 | # could be a String, Symbol or an Array of Strings, 15 | # Symbols 16 | photo.copy_tags_from(target, 'icc_profile') 17 | rescue MiniExiftool::Error => e 18 | $stderr.puts e.message 19 | exit -1 20 | end 21 | -------------------------------------------------------------------------------- /examples/external_photo.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'open-uri' 3 | require 'rubygems' 4 | require 'mini_exiftool' 5 | 6 | unless ARGV.size == 1 7 | puts "usage: ruby #{__FILE__} URI" 8 | puts " i.e.: ruby #{__FILE__} http://www.23hq.com/janfri/photo/1535332/large" 9 | exit -1 10 | end 11 | 12 | # Fetch an external photo 13 | filename = open(ARGV.first).path 14 | 15 | # Read the metadata 16 | photo = MiniExiftool.new filename 17 | 18 | # Print the metadata 19 | photo.tags.sort.each do |tag| 20 | # puts "#{tag}: #{photo[tag]}" 21 | puts tag.ljust(28) + photo[tag].to_s 22 | end 23 | -------------------------------------------------------------------------------- /test/test_composite.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestComposite < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool = MiniExiftool.new @filename_test, :composite => false 10 | @mini_exiftool_c = MiniExiftool.new @filename_test 11 | end 12 | 13 | def test_composite_tags 14 | assert_equal false, @mini_exiftool.tags.include?('Aperture') 15 | assert_equal true, @mini_exiftool_c.tags.include?('Aperture') 16 | assert_equal 9.5, @mini_exiftool_c['Aperture'] 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/test_read_coordinates.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestReadCoordinates < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test_coordinates.jpg' 9 | end 10 | 11 | def test_access_coordinates 12 | mini_exiftool_coord = MiniExiftool.new @filename_test, :coord_format => "%.6f degrees" 13 | assert_match /^43.653167 degrees/, mini_exiftool_coord['GPSLatitude'] 14 | assert_match /^79.373167 degrees/, mini_exiftool_coord['GPSLongitude'] 15 | assert_match /^43.653167 degrees.*, 79.373167 degrees/, mini_exiftool_coord['GPSPosition'] 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /test/helpers_for_test.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'mini_exiftool' 3 | require 'test/unit' 4 | require 'fileutils' 5 | require 'tempfile' 6 | begin 7 | require 'turn' 8 | rescue LoadError 9 | begin 10 | require 'rubygems' 11 | require 'turn' 12 | rescue LoadError 13 | end 14 | end 15 | 16 | include Test::Unit 17 | 18 | module TempfileTest 19 | def setup 20 | @temp_file = Tempfile.new('test') 21 | @temp_filename = @temp_file.path 22 | @data_dir = File.dirname(__FILE__) + '/data' 23 | end 24 | 25 | def teardown 26 | @temp_file.close 27 | end 28 | 29 | def assert_md5 md5, filename 30 | assert_equal md5, Digest::MD5.hexdigest(File.read(filename)) 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /test/test_pstore.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestPstore < TestCase 5 | 6 | def test_pstore 7 | pstore_dir = Dir.mktmpdir 8 | s = MiniExiftool.writable_tags.size.to_s 9 | cmd = %Q(#{RUBY_ENGINE} -EUTF-8 -I lib -r mini_exiftool -e "MiniExiftool.pstore_dir = '#{pstore_dir}'; p MiniExiftool.writable_tags.size") 10 | a = Time.now 11 | result = `#{cmd}` 12 | b = Time.now 13 | assert_equal s, result.chomp 14 | assert_equal 1, Dir[File.join(pstore_dir, '*')].size 15 | c = Time.now 16 | result = `#{cmd}` 17 | d = Time.now 18 | assert_equal s, result.chomp 19 | assert 10 * (d - c) < (b - a) 20 | ensure 21 | FileUtils.rm_rf pstore_dir 22 | end 23 | 24 | end 25 | -------------------------------------------------------------------------------- /examples/print_portraits.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'rubygems' 3 | require 'mini_exiftool' 4 | 5 | unless ARGV.size > 0 6 | puts "usage: ruby #{__FILE__} FILES" 7 | puts " i.e.: ruby #{__FILE__} *.jpg" 8 | exit -1 9 | end 10 | 11 | # Loop at all given files 12 | ARGV.each do |filename| 13 | # If a given file isn't a photo MiniExiftool new method will throw 14 | # an exception this we will catch 15 | begin 16 | photo = MiniExiftool.new filename 17 | height = photo.image_height 18 | width = photo.image_width 19 | # We define portait as a photo wich ratio of height to width is 20 | # larger than 0.7 21 | if height / width > 0.7 22 | puts filename 23 | end 24 | rescue MiniExiftool::Error => e 25 | $stderr.puts e.message 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /examples/shift_time.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'rubygems' 3 | require 'mini_exiftool' 4 | 5 | if ARGV.size < 2 6 | puts "usage: ruby #{__FILE__} [+|-]SECONDS FILES" 7 | puts " i.e.: ruby #{__FILE__} 3600 *.jpg" 8 | exit -1 9 | end 10 | 11 | delta = ARGV.shift.to_i 12 | 13 | ARGV.each do |filename| 14 | begin 15 | photo = MiniExiftool.new filename 16 | rescue MiniExiftool::Error => e 17 | $stderr.puts e.message 18 | exit -1 19 | end 20 | time = photo.date_time_original 21 | # time is a Time object, so we can use the methods of it :) 22 | photo.date_time_original = time + delta 23 | save_ok = photo.save 24 | if save_ok 25 | fmt = '%Y-%m-%d %H:%M:%S' 26 | puts "#{filename} changed: #{time.strftime(fmt)} -> #{(time + delta).strftime(fmt)}" 27 | else 28 | puts "#{filename} could not be changed" 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/test_invalid_byte_sequence_in_utf8.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | require 'json' 4 | 5 | # Thanks to Chris Salzberg (aka shioyama) and 6 | # Robert May (aka robotmay) for precious hints 7 | 8 | class TestInvalidByteSequenceInUtf8 < TestCase 9 | 10 | def setup 11 | @json = File.read(File.dirname(__FILE__) + '/data/invalid_byte_sequence_in_utf8.json') 12 | end 13 | 14 | def test_invalid_byte_sequence_gets_unconverted_value_with_invalid_encoding 15 | assert_nothing_raised do 16 | mini_exiftool = MiniExiftool.from_json(@json) 17 | assert_equal 1561, mini_exiftool.color_balance_unknown.size 18 | end 19 | end 20 | 21 | def test_replace_invalid_chars 22 | assert_nothing_raised do 23 | mini_exiftool = MiniExiftool.from_json(@json, :replace_invalid_chars => '') 24 | assert_equal 1036, mini_exiftool.color_balance_unknown.size 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /test/test_bad_preview_ifd.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestBadPreviewIFD < TestCase 5 | 6 | include TempfileTest 7 | 8 | def setup 9 | super 10 | @org_filename = @data_dir + '/Bad_PreviewIFD.jpg' 11 | FileUtils.cp @org_filename, @temp_filename 12 | @bad_preview_ifd = MiniExiftool.new @temp_filename 13 | end 14 | 15 | # Feature request rubyforge [#29587] 16 | # Thanks to Michael Grove for reporting 17 | def test_m_option 18 | title = 'anything' 19 | @bad_preview_ifd.title = title 20 | assert_equal false, @bad_preview_ifd.save, '-m option seems to be not neccessary' 21 | @bad_preview_ifd.reload 22 | @bad_preview_ifd.title = title 23 | @bad_preview_ifd.ignore_minor_errors = true 24 | assert_equal true, @bad_preview_ifd.save, 'Error while saving' 25 | @bad_preview_ifd.reload 26 | assert_equal title, @bad_preview_ifd.title 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /test/test_from_hash.rb: -------------------------------------------------------------------------------- 1 | require 'helpers_for_test' 2 | require 'json' 3 | 4 | class TestFromHash < TestCase 5 | def setup 6 | @data_dir = File.dirname(__FILE__) + '/data' 7 | hash_data = JSON.parse(File.read( @data_dir + '/test.jpg.json')).first 8 | @mini_exiftool = MiniExiftool.from_hash hash_data 9 | end 10 | 11 | def test_conversion 12 | assert_kind_of String, @mini_exiftool.model 13 | assert_kind_of Time, @mini_exiftool['DateTimeOriginal'] 14 | assert_kind_of Float, @mini_exiftool['MaxApertureValue'] 15 | assert_kind_of String, @mini_exiftool.flash 16 | assert_kind_of Integer, @mini_exiftool['ExposureCompensation'] 17 | assert_kind_of String, (@mini_exiftool['SubjectLocation'] || @mini_exiftool['SubjectArea']) 18 | assert_kind_of Array, @mini_exiftool['Keywords'] 19 | assert_kind_of String, @mini_exiftool['SupplementalCategories'] 20 | assert_kind_of Rational, @mini_exiftool.shutterspeed 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/test_encodings.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestEncodings < TestCase 5 | 6 | include TempfileTest 7 | 8 | def setup 9 | super 10 | @data_dir = File.dirname(__FILE__) + '/data' 11 | @filename_test = @data_dir + '/test_encodings.jpg' 12 | @mini_exiftool = MiniExiftool.new @filename_test 13 | end 14 | 15 | def test_iptc_encoding 16 | object_name = "Möhre" 17 | assert_not_equal object_name, @mini_exiftool.object_name 18 | correct_iptc = MiniExiftool.new(@filename_test, iptc_encoding: 'MacRoman') 19 | assert_equal object_name, correct_iptc.object_name 20 | FileUtils.cp(@filename_test, @temp_filename) 21 | correct_iptc_write = MiniExiftool.new(@temp_filename, iptc_encoding: 'MacRoman') 22 | caption = 'Das ist eine Möhre' 23 | correct_iptc_write.caption_abstract = caption 24 | correct_iptc_write.save! 25 | correct_iptc_write.reload 26 | assert_equal object_name, correct_iptc_write.object_name 27 | assert_equal caption, correct_iptc_write.caption_abstract 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /test/test_special.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestSpecial < TestCase 5 | 6 | include TempfileTest 7 | 8 | CAPTION_ABSTRACT = 'Some text for caption abstract' 9 | 10 | def setup 11 | super 12 | @org_filename = @data_dir + '/Canon.jpg' 13 | FileUtils.cp @org_filename, @temp_filename 14 | @canon = MiniExiftool.new @temp_filename 15 | end 16 | 17 | # Catching bug [#8073] 18 | # Thanks to Eric Young 19 | def test_special_chars 20 | assert_not_nil @canon['Self-timer'] 21 | assert_not_nil @canon.self_timer 22 | # preserving the original tag name 23 | assert @canon.tags.include?('Self-timer') || @canon.tags.include?('SelfTimer') 24 | assert !@canon.tags.include?('self_timer') 25 | end 26 | 27 | # Catching bug with writing caption-abstract 28 | # Thanks to Robin Romahn 29 | def test_caption_abstract_sensitive 30 | @canon['caption-abstract'] = CAPTION_ABSTRACT 31 | assert @canon.changed_tags.include?('Caption-Abstract') 32 | assert @canon.save 33 | assert_equal CAPTION_ABSTRACT, @canon.caption_abstract 34 | end 35 | 36 | def test_caption_abstract_non_sesitive 37 | @canon.caption_abstract = CAPTION_ABSTRACT.reverse 38 | assert @canon.save 39 | assert_equal CAPTION_ABSTRACT.reverse, @canon.caption_abstract 40 | end 41 | 42 | end 43 | -------------------------------------------------------------------------------- /test/test_read_numerical.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestReadNumerical < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool_num = MiniExiftool.new @filename_test, :numerical => true 10 | end 11 | 12 | def test_access_numerical 13 | assert_equal 'DYNAX 7D', @mini_exiftool_num['Model'] 14 | assert_equal 'MLT0', @mini_exiftool_num['maker_note_version'] 15 | assert_equal 'MLT0', @mini_exiftool_num[:MakerNoteVersion] 16 | assert_equal 'MLT0', @mini_exiftool_num[:maker_note_version] 17 | assert_equal 'MLT0', @mini_exiftool_num.maker_note_version 18 | assert_equal 400, @mini_exiftool_num.iso 19 | end 20 | 21 | def test_conversion_numerical 22 | assert_kind_of String, @mini_exiftool_num.model 23 | assert_kind_of Time, @mini_exiftool_num['DateTimeOriginal'] 24 | assert_kind_of Float, @mini_exiftool_num['MaxApertureValue'] 25 | assert_kind_of Integer, @mini_exiftool_num.flash 26 | assert_kind_of String, @mini_exiftool_num.exif_version 27 | assert_kind_of Integer, @mini_exiftool_num['ExposureCompensation'] 28 | assert_kind_of String, (@mini_exiftool_num['SubjectLocation'] || @mini_exiftool_num['SubjectArea']) 29 | assert_kind_of Array, @mini_exiftool_num['Keywords'] 30 | assert_kind_of String, @mini_exiftool_num['SupplementalCategories'] 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /test/test_special_dates.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'date' 3 | require 'helpers_for_test' 4 | 5 | class TestSpecialDates < TestCase 6 | 7 | include TempfileTest 8 | 9 | def setup 10 | super 11 | @org_filename = @data_dir + '/test_special_dates.jpg' 12 | FileUtils.cp @org_filename, @temp_filename 13 | @mini_exiftool = MiniExiftool.new @temp_filename 14 | @mini_exiftool_datetime = MiniExiftool.new @temp_filename, 15 | :timestamps => DateTime 16 | end 17 | 18 | # Catching bug [#16328] (1st part) 19 | # Thanks to unknown 20 | def test_datetime 21 | datetime_original = @mini_exiftool.datetime_original 22 | if datetime_original 23 | assert_kind_of Time, datetime_original 24 | else 25 | assert_equal false, datetime_original 26 | end 27 | assert_kind_of DateTime, @mini_exiftool_datetime.datetime_original 28 | assert_raise MiniExiftool::Error do 29 | @mini_exiftool.timestamps = String 30 | @mini_exiftool.reload 31 | end 32 | @mini_exiftool.timestamps = DateTime 33 | @mini_exiftool.reload 34 | assert_equal @mini_exiftool_datetime.datetime_original, 35 | @mini_exiftool.datetime_original 36 | end 37 | 38 | # Catching bug [#16328] (2nd part) 39 | # Thanks to Cecil Coupe 40 | def test_invalid_date 41 | assert_equal false, @mini_exiftool.modify_date 42 | end 43 | 44 | def test_time_zone 45 | s = '1961-08-13 12:08:25+01:00' 46 | assert_equal Time.parse(s), @mini_exiftool.preview_date_time 47 | assert_equal DateTime.parse(s), 48 | @mini_exiftool_datetime.preview_date_time 49 | end 50 | 51 | end 52 | 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rim/tire' 2 | require 'rim/version' 3 | require 'regtest/task' 4 | 5 | $:.unshift 'lib' 6 | require 'mini_exiftool' 7 | 8 | Rim.setup do |p| 9 | p.name = 'mini_exiftool' 10 | p.version = MiniExiftool::VERSION 11 | p.authors = 'Jan Friedrich' 12 | p.email = 'janfri26@gmail.com' 13 | p.summary = 'This library is a wrapper for the ExifTool command-line application (http://www.sno.phy.queensu.ca/~phil/exiftool).' 14 | p.description <<-END 15 | This library is a wrapper for the ExifTool command-line application 16 | (http://www.sno.phy.queensu.ca/~phil/exiftool) written by Phil Harvey. 17 | It provides the full power of ExifTool to Ruby: reading and writing of 18 | EXIF-data, IPTC-data and XMP-data. 19 | END 20 | p.homepage = 'https://github.com/janfri/mini_exiftool' 21 | p.license = 'LGPL-2.1' 22 | p.gem_files << 'Tutorial.rdoc' 23 | p.gem_files += FileList.new('examples/**') 24 | p.install_message = <<-END 25 | +-----------------------------------------------------------------------+ 26 | | Please ensure you have installed exiftool at least version 7.65 | 27 | | and it's found in your PATH (Try "exiftool -ver" on your commandline).| 28 | | For more details see | 29 | | http://www.sno.phy.queensu.ca/~phil/exiftool/install.html | 30 | | You need also Ruby 1.9 or higher. | 31 | | If you need support for Ruby 1.8 or exiftool prior 7.65 install | 32 | | mini_exiftool version < 2.0.0. | 33 | +-----------------------------------------------------------------------+ 34 | END 35 | p.test_warning = false 36 | p.development_dependencies << 'rake' << 'regtest' << 'test-unit' 37 | end 38 | -------------------------------------------------------------------------------- /test/test_io.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | require 'stringio' 4 | 5 | class TestIo < TestCase 6 | 7 | def test_simple_case 8 | io = open_real_io 9 | mini_exiftool = MiniExiftool.new(io) 10 | assert_equal false, io.closed?, 'IO should not be closed.' 11 | assert_equal 400, mini_exiftool.iso 12 | end 13 | 14 | def test_non_readable_io 15 | assert_raises MiniExiftool::Error do 16 | begin 17 | MiniExiftool.new($stdout) 18 | rescue MiniExiftool::Error => e 19 | assert_equal 'IO is not readable.', e.message 20 | raise e 21 | end 22 | end 23 | end 24 | 25 | def test_no_writing_when_using_io 26 | io = open_real_io 27 | m = MiniExiftool.new(io) 28 | m.iso = 100 29 | assert_raises MiniExiftool::Error do 30 | begin 31 | m.save 32 | rescue MiniExiftool::Error => e 33 | assert_equal 'No writing support when using an IO.', e.message 34 | raise e 35 | end 36 | end 37 | end 38 | 39 | def test_fast_options 40 | $DEBUG = true 41 | s = StringIO.new 42 | $stderr = s 43 | MiniExiftool.new open_real_io 44 | s.rewind 45 | assert_match /^exiftool -j "-"$/, s.read 46 | s = StringIO.new 47 | $stderr = s 48 | MiniExiftool.new open_real_io, :fast => true 49 | s.rewind 50 | assert_match /^exiftool -j -fast "-"$/, s.read 51 | s = StringIO.new 52 | $stderr = s 53 | MiniExiftool.new open_real_io, :fast2 => true 54 | s.rewind 55 | assert_match /^exiftool -j -fast2 "-"$/, s.read 56 | ensure 57 | $DEBUG = false 58 | $stderr = STDERR 59 | end 60 | 61 | protected 62 | 63 | def open_real_io 64 | File.open(File.join(File.dirname(__FILE__), 'data', 'test.jpg'), 'r') 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/test_filename_access.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | require 'rbconfig' 4 | require 'tmpdir' 5 | 6 | # Thanks to uwe58 and others for hints 7 | 8 | class TestFilenameAccess < TestCase 9 | 10 | @@running_on_windows = /mswin|mingw|cygwin/ === RbConfig::CONFIG['host_os'] 11 | 12 | @@fs_enc = Encoding.find('filesystem') 13 | 14 | def create_testfile(basename_new) 15 | tmpdir = Dir.tmpdir 16 | filename_org = File.join(File.dirname(__FILE__), 'data/test.jpg') 17 | filename_new = File.join(tmpdir, basename_new) 18 | FileUtils.cp filename_org, filename_new.encode(@@fs_enc) 19 | filename_new 20 | end 21 | 22 | def do_testing_with(basename) 23 | filename_test = create_testfile(basename) 24 | # read 25 | m = MiniExiftool.new filename_test 26 | assert_equal 400, m.iso 27 | # save 28 | m.iso = 200 29 | m.save 30 | assert_equal 200, m.iso 31 | # Check original filename maybe with other encoding than filesystem 32 | assert_equal basename, File.basename(m.filename) 33 | rescue Exception => e 34 | assert false, "File #{filename_test.inspect} not found!" 35 | end 36 | 37 | def test_access_filename_with_spaces 38 | do_testing_with 'filename with spaces.jpg' 39 | end 40 | 41 | def test_access_filename_with_special_chars 42 | do_testing_with 'filename_with_Ümläüts.jpg' 43 | end 44 | 45 | unless @@running_on_windows 46 | def test_access_filename_with_doublequotes 47 | do_testing_with 'filename_with_"doublequotes"_inside.jpg' 48 | end 49 | end 50 | 51 | def test_access_filename_with_dollar_sign 52 | # Thanks to Michael Dungan for the hint 53 | do_testing_with 'filename_with_$_sign.jpg' 54 | end 55 | 56 | def test_access_filename_with_ampersand 57 | do_testing_with 'filename_with_&_sign.jpg' 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /test/test_read.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestRead < TestCase 5 | 6 | def setup 7 | @data_dir = File.dirname(__FILE__) + '/data' 8 | @filename_test = @data_dir + '/test.jpg' 9 | @mini_exiftool = MiniExiftool.new @filename_test 10 | end 11 | 12 | def test_access 13 | assert_equal 'DYNAX 7D', @mini_exiftool['Model'] 14 | assert_equal 'MLT0', @mini_exiftool['maker_note_version'] 15 | assert_equal 'MLT0', @mini_exiftool[:MakerNoteVersion] 16 | assert_equal 'MLT0', @mini_exiftool[:maker_note_version] 17 | assert_equal 'MLT0', @mini_exiftool.maker_note_version 18 | assert_equal 400, @mini_exiftool.iso 19 | end 20 | 21 | def test_tags 22 | assert @mini_exiftool.tags.include?('FileSize') 23 | end 24 | 25 | def test_conversion 26 | assert_kind_of String, @mini_exiftool.model 27 | assert_kind_of Time, @mini_exiftool['DateTimeOriginal'] 28 | assert_kind_of Float, @mini_exiftool['MaxApertureValue'] 29 | assert_kind_of String, @mini_exiftool.flash 30 | assert_kind_of Integer, @mini_exiftool['ExposureCompensation'] 31 | assert_kind_of String, (@mini_exiftool['SubjectLocation'] || @mini_exiftool['SubjectArea']) 32 | assert_kind_of Array, @mini_exiftool['Keywords'] 33 | assert_kind_of String, @mini_exiftool['SupplementalCategories'] 34 | assert_kind_of Rational, @mini_exiftool.shutterspeed 35 | end 36 | 37 | def test_list_tags 38 | assert_equal ['Orange', 'Rot'], @mini_exiftool['Keywords'] 39 | assert_equal 'Natur', @mini_exiftool['SupplementalCategories'] 40 | assert_equal ['Natur'], Array(@mini_exiftool['SupplementalCategories']) 41 | end 42 | 43 | def test_value_encoding 44 | title= 'Abenddämmerung' 45 | assert_equal Encoding::UTF_8, @mini_exiftool.title.encoding 46 | assert_equal title, @mini_exiftool.title 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /test/test_copy_tags_from.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestCopyTagsFrom < TestCase 5 | 6 | include TempfileTest 7 | 8 | def setup 9 | super 10 | @canon_filename = @data_dir + '/Canon.jpg' 11 | FileUtils.cp(@canon_filename, @temp_filename) 12 | @mini_exiftool = MiniExiftool.new(@temp_filename) 13 | @source_filename = @data_dir + '/test.jpg' 14 | @canon_md5 = Digest::MD5.hexdigest(File.read(@canon_filename)) 15 | @source_md5 = Digest::MD5.hexdigest(File.read(@source_filename)) 16 | end 17 | 18 | def test_single_tag 19 | assert_nil @mini_exiftool.title 20 | res = @mini_exiftool.copy_tags_from(@source_filename, :title) 21 | assert res 22 | assert_equal 'Abenddämmerung', @mini_exiftool.title 23 | assert_md5 @source_md5, @source_filename 24 | end 25 | 26 | def test_more_than_one_tag 27 | assert_nil @mini_exiftool.title 28 | assert_nil @mini_exiftool.keywords 29 | res = @mini_exiftool.copy_tags_from(@source_filename, %w[title keywords]) 30 | assert res 31 | assert_equal 'Abenddämmerung', @mini_exiftool.title 32 | assert_equal %w[Orange Rot], @mini_exiftool.keywords 33 | assert_md5 @source_md5, @source_filename 34 | end 35 | 36 | def test_non_existing_sourcefile 37 | assert_raises MiniExiftool::Error do 38 | @mini_exiftool.copy_tags_from('non_existend_file', :title) 39 | end 40 | assert_md5 @source_md5, @source_filename 41 | end 42 | 43 | def test_non_existend_tag 44 | @mini_exiftool.copy_tags_from(@source_filename, :non_existend_tag) 45 | assert_md5 @canon_md5, @canon_filename 46 | assert_md5 @source_md5, @source_filename 47 | end 48 | 49 | def test_non_writable_tag 50 | @mini_exiftool.copy_tags_from(@source_filename, 'JFIF') 51 | assert_md5 @canon_md5, @canon_filename 52 | assert_md5 @source_md5, @source_filename 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /test/test_class_methods.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | 4 | class TestClassMethods < TestCase 5 | 6 | def test_new 7 | assert_nothing_raised do 8 | @mini_exiftool = MiniExiftool.new 9 | end 10 | assert_equal nil, @mini_exiftool.filename 11 | assert_nothing_raised do 12 | MiniExiftool.new nil 13 | end 14 | assert_raises MiniExiftool::Error do 15 | MiniExiftool.new false 16 | end 17 | assert_raises MiniExiftool::Error do 18 | MiniExiftool.new '' 19 | end 20 | assert_raises MiniExiftool::Error do 21 | MiniExiftool.new 'not_existing_file' 22 | end 23 | assert_raises MiniExiftool::Error do 24 | MiniExiftool.new '.' # directory 25 | end 26 | begin 27 | MiniExiftool.new 'not_existing_file' 28 | rescue MiniExiftool::Error => e 29 | assert_match /File 'not_existing_file' does not exist/, e.message 30 | end 31 | end 32 | 33 | def test_command 34 | cmd = MiniExiftool.command 35 | assert_equal 'exiftool', cmd 36 | MiniExiftool.command = 'non_existend' 37 | assert_equal 'non_existend', MiniExiftool.command 38 | assert_raises MiniExiftool::Error do 39 | met = MiniExiftool.new(File.join(File.dirname(__FILE__), 40 | 'data/test.jpg')) 41 | end 42 | MiniExiftool.command = cmd 43 | end 44 | 45 | def test_opts 46 | opts = MiniExiftool.opts 47 | assert_kind_of Hash, opts 48 | begin 49 | org = MiniExiftool.opts[:composite] 50 | met1 = MiniExiftool.new 51 | MiniExiftool.opts[:composite] = !org 52 | met2 = MiniExiftool.new 53 | MiniExiftool.opts[:composite] = org 54 | met3 = MiniExiftool.new 55 | assert_equal org, met1.composite 56 | assert_equal !org, met2.composite 57 | assert_equal org, met1.composite 58 | ensure 59 | MiniExiftool.opts[:composite] = org 60 | end 61 | end 62 | 63 | def test_all_tags 64 | all_tags = MiniExiftool.all_tags 65 | assert all_tags.include?('ISO') 66 | assert all_tags.include?('ExifToolVersion') 67 | end 68 | 69 | def test_writable_tags 70 | w_tags = MiniExiftool.writable_tags 71 | assert w_tags.include?('ISO') 72 | assert ! w_tags.include?('ExifToolVersion') 73 | end 74 | 75 | def test_exiftool_version 76 | v = MiniExiftool.exiftool_version 77 | assert_match /\A\d+\.\d+\z/, v 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /test/test_save.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'digest/md5' 3 | require 'fileutils' 4 | require 'tempfile' 5 | require 'helpers_for_test' 6 | 7 | class TestSave < TestCase 8 | 9 | include TempfileTest 10 | 11 | def setup 12 | super 13 | @org_filename = @data_dir + '/test.jpg' 14 | FileUtils.cp(@org_filename, @temp_filename) 15 | @mini_exiftool = MiniExiftool.new @temp_filename 16 | @mini_exiftool_num = MiniExiftool.new @temp_filename, :numerical => true 17 | @org_md5 = Digest::MD5.hexdigest(File.read(@org_filename)) 18 | end 19 | 20 | def test_allowed_value 21 | @mini_exiftool_num['Orientation'] = 2 22 | result = @mini_exiftool_num.save 23 | assert_equal true, result 24 | assert_equal @org_md5, Digest::MD5.hexdigest(File.read(@org_filename)) 25 | assert_not_equal @org_md5, Digest::MD5.hexdigest(File.read(@temp_filename)) 26 | assert_equal false, @mini_exiftool_num.changed? 27 | result = @mini_exiftool_num.save 28 | assert_equal false, result 29 | end 30 | 31 | def test_non_allowed_value 32 | @mini_exiftool['Orientation'] = 'some string' 33 | result = @mini_exiftool.save 34 | assert_equal false, result 35 | assert_equal 1, @mini_exiftool.errors.size 36 | assert_match(/Can't convert IFD0:Orientation \(not in PrintConv\)/, 37 | @mini_exiftool.errors['Orientation']) 38 | assert @mini_exiftool.changed? 39 | assert @mini_exiftool.changed_tags.include?('Orientation') 40 | end 41 | 42 | def test_no_changing_of_file_when_error 43 | @mini_exiftool['ISO'] = 800 44 | @mini_exiftool['Orientation'] = 'some value' 45 | @mini_exiftool['ExposureTime'] = '1/30' 46 | result = @mini_exiftool.save 47 | assert_equal false, result 48 | assert_equal @org_md5, Digest::MD5.hexdigest(File.read(@org_filename)) 49 | assert_equal @org_md5, Digest::MD5.hexdigest(File.read(@temp_filename)) 50 | end 51 | 52 | def test_value_encoding 53 | special_string = 'äöü' 54 | @mini_exiftool.title = special_string 55 | assert @mini_exiftool.save 56 | @mini_exiftool.reload 57 | assert_equal Encoding::UTF_8, @mini_exiftool.title.encoding 58 | assert_equal special_string, @mini_exiftool.title 59 | end 60 | 61 | def test_save_bang 62 | @mini_exiftool.orientation = 'some value' 63 | exception = false 64 | begin 65 | @mini_exiftool.save! 66 | rescue MiniExiftool::Error => e 67 | assert_match /Orientation/, e.message 68 | exception = true 69 | end 70 | assert exception, "No exception when save! with error." 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /test/test_dumping.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'helpers_for_test' 3 | require 'yaml' 4 | 5 | class TestDumping < TestCase 6 | 7 | def setup 8 | @data_dir = File.dirname(__FILE__) + '/data' 9 | @filename_test = @data_dir + '/test.jpg' 10 | @mini_exiftool = MiniExiftool.new @filename_test 11 | end 12 | 13 | def test_to_hash 14 | hash = @mini_exiftool.to_hash 15 | assert_equal Hash, hash.class 16 | assert_equal @mini_exiftool.tags.size, hash.size, 'Size of Hash is not correct.' 17 | assert_not_nil hash['ExifToolVersion'], 'Original name of exiftool tag is not preserved.' 18 | all_ok = true 19 | different_tag = '' 20 | v = '' 21 | hash.each do |k,v| 22 | unless @mini_exiftool[k] == v 23 | all_ok = false 24 | different_tag = k 25 | break 26 | end 27 | end 28 | assert all_ok, "Tag #{different_tag.inspect}: expected: #{@mini_exiftool[different_tag].inspect}, actual: #{v.inspect}" 29 | end 30 | 31 | def test_from_hash 32 | hash = @mini_exiftool.to_hash 33 | mini_exiftool_new = MiniExiftool.from_hash hash 34 | assert_equal MiniExiftool, mini_exiftool_new.class 35 | assert_equal @mini_exiftool.tags.size, mini_exiftool_new.tags.size 36 | all_ok = true 37 | different_tag = '' 38 | @mini_exiftool.tags.each do |tag| 39 | unless @mini_exiftool[tag] == mini_exiftool_new[tag] 40 | all_ok = false 41 | different_tag = tag 42 | break 43 | end 44 | end 45 | assert all_ok, "Tag #{different_tag.inspect}: expected: #{@mini_exiftool[different_tag].inspect}, actual: #{mini_exiftool_new[different_tag].inspect}" 46 | 47 | end 48 | 49 | def test_to_yaml 50 | hash = @mini_exiftool.to_hash 51 | yaml = @mini_exiftool.to_yaml 52 | assert_equal hash, YAML.load(yaml) 53 | end 54 | 55 | def test_from_yaml 56 | hash = @mini_exiftool.to_hash 57 | yaml = hash.to_yaml 58 | mini_exiftool_new = MiniExiftool.from_yaml(yaml) 59 | assert_equal MiniExiftool, mini_exiftool_new.class 60 | assert_equal hash, mini_exiftool_new.to_hash 61 | end 62 | 63 | def test_heuristics_for_restoring_composite 64 | standard = @mini_exiftool.to_hash 65 | no_composite = MiniExiftool.new(@filename_test, :composite => false).to_hash 66 | assert_equal true, MiniExiftool.from_hash(standard).composite 67 | assert_equal false, MiniExiftool.from_hash(no_composite).composite 68 | assert_equal true, MiniExiftool.from_yaml(standard.to_yaml).composite 69 | assert_equal false, MiniExiftool.from_yaml(no_composite.to_yaml).composite 70 | end 71 | 72 | def test_heuristics_for_restoring_numerical 73 | standard = @mini_exiftool.to_hash 74 | numerical = MiniExiftool.new(@filename_test, :numerical => true).to_hash 75 | assert_equal false, MiniExiftool.from_hash(standard).numerical 76 | assert_equal true, MiniExiftool.from_hash(numerical).numerical 77 | assert_equal false, MiniExiftool.from_yaml(standard.to_yaml).numerical 78 | assert_equal true, MiniExiftool.from_yaml(numerical.to_yaml).numerical 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /examples/show_speedup_with_fast_option.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'mini_exiftool' 3 | require 'open3' 4 | 5 | unless ARGV.size == 1 6 | puts "usage: ruby #{__FILE__} URI" 7 | puts " i.e.: ruby #{__FILE__} http://farm6.staticflickr.com/5015/5458914734_8fd3f33278_o.jpg" 8 | exit -1 9 | end 10 | 11 | arg = ARGV.shift 12 | 13 | #################################### 14 | # Helper methods 15 | #################################### 16 | 17 | def time 18 | a = Time.now 19 | yield 20 | b = Time.now 21 | b - a 22 | end 23 | 24 | def print_statistics name, without_fast, fast, fast2 25 | puts '-' * 40 26 | puts name, "\n" 27 | puts format 'without fast: %0.2fs', without_fast 28 | puts format 'fast : %0.2fs', fast 29 | puts format 'fast2 : %0.2fs', fast2 30 | puts 31 | puts format 'speedup fast : %0.2f', without_fast / fast 32 | puts format 'speedup fast2: %0.2f', without_fast / fast2 33 | puts '-' * 40 34 | puts 35 | end 36 | 37 | #################################### 38 | # Plain Ruby with standard library 39 | #################################### 40 | 41 | require 'net/http' 42 | 43 | uri = URI(arg) 44 | 45 | def read_from_http uri, io 46 | Thread.new(uri, io) do |uri, io| 47 | Net::HTTP.start(uri.host, uri.port) do |http| 48 | request = Net::HTTP::Get.new uri 49 | http.request request do |response| 50 | response.read_body do |chunk| 51 | io.write chunk 52 | end 53 | end 54 | end 55 | io.close 56 | end 57 | end 58 | 59 | without_fast = time do 60 | output, input = IO.pipe 61 | read_from_http uri, input 62 | MiniExiftool.new output 63 | end 64 | 65 | fast = time do 66 | output, input = IO.pipe 67 | read_from_http uri, input 68 | MiniExiftool.new output, fast: true 69 | end 70 | 71 | fast2 = time do 72 | output, input = IO.pipe 73 | read_from_http uri, input 74 | MiniExiftool.new output, fast2: true 75 | end 76 | 77 | print_statistics 'net/http', without_fast, fast, fast2 78 | 79 | #################################### 80 | # curl 81 | #################################### 82 | 83 | without_fast = time do 84 | input, output = Open3.popen3("curl -s #{arg}") 85 | input.close 86 | MiniExiftool.new output 87 | end 88 | 89 | fast = time do 90 | input, output = Open3.popen3("curl -s #{arg}") 91 | input.close 92 | MiniExiftool.new output, fast: true 93 | end 94 | 95 | fast2 = time do 96 | input, output = Open3.popen3("curl -s #{arg}") 97 | input.close 98 | MiniExiftool.new output, fast2: true 99 | end 100 | 101 | print_statistics 'curl', without_fast, fast, fast2 102 | 103 | #################################### 104 | # wget 105 | #################################### 106 | 107 | without_fast = time do 108 | input, output = Open3.popen3("wget -q -O - #{arg}") 109 | input.close 110 | MiniExiftool.new output 111 | end 112 | 113 | fast = time do 114 | input, output = Open3.popen3("wget -q -O - #{arg}") 115 | input.close 116 | MiniExiftool.new output, fast: true 117 | end 118 | 119 | fast2 = time do 120 | input, output = Open3.popen3("wget -q -O - #{arg}") 121 | input.close 122 | MiniExiftool.new output, fast2: true 123 | end 124 | 125 | print_statistics 'wget', without_fast, fast, fast2 126 | 127 | -------------------------------------------------------------------------------- /test/data/test.jpg.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "SourceFile": "test/data/test.jpg", 3 | "ExifToolVersion": 8.77, 4 | "FileName": "test.jpg", 5 | "Directory": "test/data", 6 | "FileSize": "46 kB", 7 | "FileModifyDate": "2012:07:05 20:28:24-07:00", 8 | "FilePermissions": "rw-r--r--", 9 | "FileType": "JPEG", 10 | "MIMEType": "image/jpeg", 11 | "JFIFVersion": 1.01, 12 | "ExifByteOrder": "Big-endian (Motorola, MM)", 13 | "ImageDescription": "KONICA MINOLTA DIGITAL CAMERA", 14 | "Make": "KONICA MINOLTA", 15 | "Model": "DYNAX 7D", 16 | "Orientation": "Horizontal (normal)", 17 | "XResolution": 72, 18 | "YResolution": 72, 19 | "ResolutionUnit": "inches", 20 | "Software": "DYNAX 7D v1.10", 21 | "ModifyDate": "2005:09:13 20:08:50", 22 | "YCbCrPositioning": "Centered", 23 | "ExposureTime": "1/60", 24 | "FNumber": 9.5, 25 | "ExposureProgram": "Program AE", 26 | "ISO": 400, 27 | "ExifVersion": "0221", 28 | "DateTimeOriginal": "2005:09:13 20:08:50", 29 | "CreateDate": "2005:09:13 20:08:50", 30 | "ComponentsConfiguration": "Y, Cb, Cr, -", 31 | "BrightnessValue": 4.5, 32 | "ExposureCompensation": -1, 33 | "MaxApertureValue": 4.5, 34 | "MeteringMode": "Multi-segment", 35 | "LightSource": "Unknown", 36 | "Flash": "Off, Did not fire", 37 | "FocalLength": "75.0 mm", 38 | "SubjectArea": "1504 1000 256 304", 39 | "MakerNoteVersion": "MLT0", 40 | "MinoltaImageSize": "Large", 41 | "WhiteBalance": "Auto", 42 | "FocusMode": "AF-A", 43 | "AFPoints": "Center", 44 | "FlashMode": "Normal", 45 | "ISOSetting": 400, 46 | "FreeMemoryCardImages": 202, 47 | "HueAdjustment": 0, 48 | "Rotation": "Horizontal (normal)", 49 | "ImageNumber": 6, 50 | "NoiseReduction": "Unknown (2)", 51 | "ImageNumber2": 50, 52 | "ZoneMatchingOn": "Off", 53 | "CompressedImageSize": 1598477, 54 | "PreviewImageStart": 39152, 55 | "PreviewImageLength": 0, 56 | "SceneMode": "Standard", 57 | "ColorMode": "Natural sRGB", 58 | "MinoltaQuality": "Fine", 59 | "FlashExposureComp": 0, 60 | "Teleconverter": "None", 61 | "ImageStabilization": "On", 62 | "ZoneMatching": "ISO Setting Used", 63 | "ColorTemperature": 0, 64 | "LensType": "Minolta AF 28-135mm F4-4.5 or Sigma Lens", 65 | "UserComment": "", 66 | "FlashpixVersion": "0100", 67 | "ColorSpace": "sRGB", 68 | "ExifImageWidth": 3008, 69 | "ExifImageHeight": 2000, 70 | "CustomRendered": "Normal", 71 | "ExposureMode": "Auto", 72 | "DigitalZoomRatio": 0, 73 | "FocalLengthIn35mmFormat": "112 mm", 74 | "SceneCaptureType": "Standard", 75 | "GainControl": "Low gain up", 76 | "Contrast": "Normal", 77 | "Saturation": "Normal", 78 | "Sharpness": "Normal", 79 | "PrintIMVersion": "0300", 80 | "Compression": "JPEG (old-style)", 81 | "ThumbnailOffset": 39274, 82 | "ThumbnailLength": 1820, 83 | "CurrentIPTCDigest": "dd8d51d28ddf04f08f870e5ff2f64d01", 84 | "Keywords": ["Orange","Rot"], 85 | "ApplicationRecordVersion": 4, 86 | "SupplementalCategories": "Natur", 87 | "XMPToolkit": "Image::ExifTool 7.03", 88 | "Title": "Abenddämmerung", 89 | "ImageWidth": 300, 90 | "ImageHeight": 199, 91 | "EncodingProcess": "Baseline DCT, Huffman coding", 92 | "BitsPerSample": 8, 93 | "ColorComponents": 3, 94 | "YCbCrSubSampling": "YCbCr4:2:0 (2 2)", 95 | "Aperture": 9.5, 96 | "ImageSize": "300x199", 97 | "LensID": "Minolta AF 28-135mm F4-4.5", 98 | "ScaleFactor35efl": 1.5, 99 | "ShutterSpeed": "1/60", 100 | "ThumbnailImage": "(Binary data 1820 bytes)", 101 | "CircleOfConfusion": "0.020 mm", 102 | "FOV": "18.3 deg", 103 | "FocalLength35efl": "75.0 mm (35 mm equivalent: 112.0 mm)", 104 | "HyperfocalDistance": "29.43 m", 105 | "LightValue": 10.4 106 | }] 107 | -------------------------------------------------------------------------------- /mini_exiftool.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | # stub: mini_exiftool 2.8.0 ruby lib 3 | # 4 | # This file is automatically generated by rim. 5 | # PLEASE DO NOT EDIT IT DIRECTLY! 6 | # Change instead the values in Rim.setup in Rakefile. 7 | 8 | Gem::Specification.new do |s| 9 | s.name = "mini_exiftool".freeze 10 | s.version = "2.8.0" 11 | 12 | s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 13 | s.require_paths = ["lib".freeze] 14 | s.authors = ["Jan Friedrich".freeze] 15 | s.date = "2016-09-12" 16 | s.description = "This library is a wrapper for the ExifTool command-line application\n(http://www.sno.phy.queensu.ca/~phil/exiftool) written by Phil Harvey.\nIt provides the full power of ExifTool to Ruby: reading and writing of\nEXIF-data, IPTC-data and XMP-data.\n".freeze 17 | s.email = "janfri26@gmail.com".freeze 18 | s.files = ["COPYING".freeze, "Changelog".freeze, "README.rdoc".freeze, "Rakefile".freeze, "Tutorial.rdoc".freeze, "examples/copy_icc_profile.rb".freeze, "examples/external_photo.rb".freeze, "examples/print_portraits.rb".freeze, "examples/shift_time.rb".freeze, "examples/show_speedup_with_fast_option.rb".freeze, "lib/mini_exiftool.rb".freeze, "test/data".freeze, "test/data/Bad_PreviewIFD.jpg".freeze, "test/data/Canon.jpg".freeze, "test/data/INFORMATION".freeze, "test/data/invalid_byte_sequence_in_utf8.json".freeze, "test/data/invalid_rational.json".freeze, "test/data/test.jpg".freeze, "test/data/test.jpg.json".freeze, "test/data/test_coordinates.jpg".freeze, "test/data/test_encodings.jpg".freeze, "test/data/test_special_dates.jpg".freeze, "test/helpers_for_test.rb".freeze, "test/test_bad_preview_ifd.rb".freeze, "test/test_class_methods.rb".freeze, "test/test_composite.rb".freeze, "test/test_copy_tags_from.rb".freeze, "test/test_dumping.rb".freeze, "test/test_encodings.rb".freeze, "test/test_filename_access.rb".freeze, "test/test_from_hash.rb".freeze, "test/test_invalid_byte_sequence_in_utf8.rb".freeze, "test/test_invalid_rational.rb".freeze, "test/test_io.rb".freeze, "test/test_pstore.rb".freeze, "test/test_read.rb".freeze, "test/test_read_coordinates.rb".freeze, "test/test_read_numerical.rb".freeze, "test/test_save.rb".freeze, "test/test_special.rb".freeze, "test/test_special_dates.rb".freeze, "test/test_write.rb".freeze] 19 | s.homepage = "https://github.com/janfri/mini_exiftool".freeze 20 | s.licenses = ["LGPL-2.1".freeze] 21 | s.post_install_message = "+-----------------------------------------------------------------------+\n| Please ensure you have installed exiftool at least version 7.65 |\n| and it's found in your PATH (Try \"exiftool -ver\" on your commandline).|\n| For more details see |\n| http://www.sno.phy.queensu.ca/~phil/exiftool/install.html |\n| You need also Ruby 1.9 or higher. |\n| If you need support for Ruby 1.8 or exiftool prior 7.65 install |\n| mini_exiftool version < 2.0.0. |\n+-----------------------------------------------------------------------+\n".freeze 22 | s.rubygems_version = "2.6.6".freeze 23 | s.summary = "This library is a wrapper for the ExifTool command-line application (http://www.sno.phy.queensu.ca/~phil/exiftool).".freeze 24 | 25 | if s.respond_to? :specification_version then 26 | s.specification_version = 4 27 | 28 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 29 | s.add_development_dependency(%q.freeze, ["~> 2.9"]) 30 | s.add_development_dependency(%q.freeze, [">= 0"]) 31 | s.add_development_dependency(%q.freeze, [">= 0"]) 32 | s.add_development_dependency(%q.freeze, [">= 0"]) 33 | else 34 | s.add_dependency(%q.freeze, ["~> 2.9"]) 35 | s.add_dependency(%q.freeze, [">= 0"]) 36 | s.add_dependency(%q.freeze, [">= 0"]) 37 | s.add_dependency(%q.freeze, [">= 0"]) 38 | end 39 | else 40 | s.add_dependency(%q.freeze, ["~> 2.9"]) 41 | s.add_dependency(%q.freeze, [">= 0"]) 42 | s.add_dependency(%q.freeze, [">= 0"]) 43 | s.add_dependency(%q.freeze, [">= 0"]) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = MiniExiftool 2 | 3 | This library is a wrapper for the ExifTool command-line application 4 | (http://www.sno.phy.queensu.ca/~phil/exiftool) written by Phil Harvey. 5 | It provides the full power of ExifTool to Ruby: reading and writing of 6 | EXIF-data, IPTC-data and XMP-data. 7 | 8 | == Requirements 9 | 10 | Ruby 1.9 or higher and an installation of the ExifTool 11 | command-line application at least version 7.65. 12 | If you run on Ruby 1.8 or with a prior exiftool version 13 | install mini_exiftool version 1.x.x. 14 | Instructions for installation you can find under 15 | http://www.sno.phy.queensu.ca/~phil/exiftool/install.html . 16 | 17 | Alternatively Wil Gieseler has bundled a meta-gem that eliminates the 18 | need for a separate ExifTool installation. Have a look at 19 | http://github.com/wilg/mini_exiftool_vendored or 20 | http://rubygems.org/gems/mini_exiftool_vendored . 21 | 22 | == Installation 23 | 24 | First you need ExifTool (see under Requirements above). Then you can simply 25 | install the gem with 26 | gem install mini_exiftool 27 | 28 | If you need to support older versions of Ruby or exiftool (see Requirements above) 29 | gem install --version "< 2.0.0" mini_exiftool 30 | 31 | == Configuration 32 | 33 | You can manually set the exiftool command that should be used via 34 | MiniExiftool.command = '/path/to/my/exiftool' 35 | 36 | In addition, you can also tell MiniExiftool where to store the PStore files with tags 37 | which exiftool supports. The PStore files are used for performance issues. 38 | Per default the PStore files are stored in a sub directory .mini_exiftool or 39 | _mini_exiftool under your home directory. 40 | MiniExiftool.pstore_dir = '/path/to/pstore/dir' 41 | 42 | If you're using Rails, this is easily done with 43 | MiniExiftool.pstore_dir = Rails.root.join('tmp').to_s 44 | 45 | Important hint: if you have to change the configuration you have to do this direct 46 | after require 'mini_exiftool'. 47 | 48 | == Usage 49 | 50 | In general MiniExiftool is very intuitive to use as the following examples show: 51 | 52 | # Reading meta data from a file 53 | photo = MiniExiftool.new 'photo.jpg' 54 | puts photo.title 55 | 56 | # Alternative reading meta data from an IO instance 57 | photo = MiniExiftool.new io 58 | puts photo.title 59 | 60 | # Writing meta data 61 | photo = MiniExiftool.new 'photo.jpg' 62 | photo.title = 'This is the new title' 63 | photo.save 64 | 65 | # Copying meta data 66 | photo = MiniExiftool.new('photo.jpg') 67 | photo.copy_tags_from('another_photo.jpg', :author) 68 | 69 | 70 | For further information about using MiniExiftool read the Tutorial.rdoc 71 | in the project root folder and have a look at the examples in directory 72 | examples. 73 | 74 | == Encodings 75 | 76 | In MiniExiftool all strings are encoded in UTF-8. If you need other 77 | encodings in your project use the String#encod* methods. 78 | 79 | If you have problems with corrupted strings when using MiniExiftool 80 | there are two reasons for this: 81 | 82 | === Internal character sets 83 | 84 | You can specify the charset in which the meta data is in the file encoded 85 | if you read or write to some sections of meta data (i.e. IPTC, XMP ...). 86 | It exists various options of the form *_encoding: exif, iptc, xmp, png, 87 | id3, pdf, photoshop, quicktime, aiff, mie and vorbis. 88 | 89 | For IPTC meta data it is recommended to set also the CodedCharacterSet 90 | tag. 91 | 92 | Please read the section about the character sets of the ExifTool command 93 | line application carefully to understand what's going on 94 | (http://www.sno.phy.queensu.ca/~phil/exiftool/faq.html#Q10)! 95 | 96 | # Using UTF-8 as internal encoding for IPTC tags and MacRoman 97 | # as internal encoding for EXIF tags 98 | photo = MiniExiftool.new('photo.jpg', iptc_encoding: 'UTF8', 99 | exif_encoding: 'MacRoman' 100 | # IPTC CaptionAbstract is already UTF-8 encoded 101 | puts photo.caption_abstract 102 | # EXIF Comment is converted from MacRoman to UTF-8 103 | puts photo.comment 104 | 105 | photo = MiniExiftool.new('photo.jpg', iptc_encoding: 'UTF8', 106 | exif_encoding: 'MacRoman' 107 | # When saving IPTC data setting CodedCharacterSet as recommended 108 | photo.coded_character_set = 'UTF8' 109 | # IPTC CaptionAbstract will be stored in UTF-8 encoding 110 | photo.caption_abstract = 'Some text with Ümläuts' 111 | # EXIF Comment will be stored in MacRoman encoding 112 | photo.comment = 'Comment with Ümläuts' 113 | photo.save 114 | 115 | === Corrupt characters 116 | 117 | You use the correct internal character set but in the string are still corrupt 118 | characters. 119 | This problem you can solve with the option replace_invalid_chars: 120 | 121 | # Replace all invalid characters with a question mark 122 | photo = MiniExiftool.new('photo.jpg', replace_invalid_chars: '?') 123 | 124 | == Contribution 125 | 126 | The code is hosted in a git repository on github at 127 | https://github.com/janfri/mini_exiftool 128 | feel free to contribute! 129 | 130 | == Author 131 | Jan Friedrich 132 | 133 | == Copyright / License 134 | Copyright (c) 2007-2016 by Jan Friedrich 135 | 136 | Licensed under terms of the GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1, 137 | February 1999 (see file COPYING for more details) 138 | -------------------------------------------------------------------------------- /test/test_write.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | require 'digest/md5' 3 | require 'fileutils' 4 | require 'tempfile' 5 | require 'helpers_for_test' 6 | 7 | class TestWrite < TestCase 8 | 9 | def setup 10 | @temp_file = Tempfile.new('test') 11 | @temp_file.close 12 | @temp_filename = @temp_file.path 13 | @org_filename = File.dirname(__FILE__) + '/data/test.jpg' 14 | FileUtils.cp(@org_filename, @temp_filename) 15 | @mini_exiftool = MiniExiftool.new @temp_filename 16 | @mini_exiftool_num = MiniExiftool.new @temp_filename, :numerical => true 17 | end 18 | 19 | def teardown 20 | @temp_file.delete 21 | end 22 | 23 | def test_access_existing_tags 24 | assert_equal 'Horizontal (normal)', @mini_exiftool['Orientation'] 25 | @mini_exiftool['Orientation'] = 'some string' 26 | assert_equal 'some string', @mini_exiftool['Orientation'] 27 | assert_equal false, @mini_exiftool.changed?('Orientation') 28 | @mini_exiftool['Orientation'] = 2 29 | assert_equal 2, @mini_exiftool['Orientation'] 30 | assert @mini_exiftool.changed_tags.include?('Orientation') 31 | @mini_exiftool.save 32 | assert_equal 'Mirror horizontal', @mini_exiftool['Orientation'] 33 | @mini_exiftool_num.reload 34 | assert_equal 2, @mini_exiftool_num['Orientation'] 35 | end 36 | 37 | def test_access_existing_tags_numerical 38 | assert_equal 1, @mini_exiftool_num['Orientation'] 39 | @mini_exiftool_num['Orientation'] = 2 40 | assert_equal 2, @mini_exiftool_num['Orientation'] 41 | assert_equal 2, @mini_exiftool_num.orientation 42 | @mini_exiftool_num.orientation = 3 43 | assert_equal 3, @mini_exiftool_num.orientation 44 | assert @mini_exiftool_num.changed_tags.include?('Orientation') 45 | @mini_exiftool_num.save 46 | assert_equal 3, @mini_exiftool_num['Orientation'] 47 | @mini_exiftool.reload 48 | assert_equal 'Rotate 180', @mini_exiftool['Orientation'] 49 | end 50 | 51 | def test_access_non_writable_tags 52 | @mini_exiftool_num['FileSize'] = 1 53 | assert_equal true, @mini_exiftool_num.changed? 54 | @mini_exiftool_num['SomeNonWritableName'] = 'test' 55 | assert_equal true, @mini_exiftool_num.changed? 56 | end 57 | 58 | # Catching rubyforge bug [#29596] 59 | # Thanks to Michael Grove for reporting 60 | # Part 1 61 | def test_quotes_in_values 62 | caption = "\"String in quotes\"" 63 | @mini_exiftool.caption = caption 64 | assert_equal true, @mini_exiftool.save, 'Saving error' 65 | @mini_exiftool.reload 66 | assert_equal caption, @mini_exiftool.caption 67 | end 68 | 69 | # Catching rubyforge bug [#29596] 70 | # Thanks to Michael Grove for reporting 71 | # Part 2 72 | def test_quotes_and_apostrophe_in_values 73 | caption = caption = "\"Watch your step, it's slippery.\"" 74 | @mini_exiftool.caption = caption 75 | assert_equal true, @mini_exiftool.save, 'Saving error' 76 | @mini_exiftool.reload 77 | assert_equal caption, @mini_exiftool.caption 78 | end 79 | 80 | def test_time_conversion 81 | t = Time.now 82 | @mini_exiftool_num['DateTimeOriginal'] = t 83 | assert_kind_of Time, @mini_exiftool_num['DateTimeOriginal'] 84 | assert_equal true, @mini_exiftool_num.changed_tags.include?('DateTimeOriginal') 85 | @mini_exiftool_num.save 86 | assert_equal false, @mini_exiftool_num.changed? 87 | assert_kind_of Time, @mini_exiftool_num['DateTimeOriginal'] 88 | assert_equal t.to_s, @mini_exiftool_num['DateTimeOriginal'].to_s 89 | end 90 | 91 | def test_float_conversion 92 | assert_kind_of Float, @mini_exiftool_num['BrightnessValue'] 93 | new_time = @mini_exiftool_num['BrightnessValue'] + 1 94 | @mini_exiftool_num['BrightnessValue'] = new_time 95 | assert_equal new_time, @mini_exiftool_num['BrightnessValue'] 96 | assert_equal true, @mini_exiftool_num.changed_tags.include?('BrightnessValue') 97 | @mini_exiftool_num.save 98 | assert_kind_of Float, @mini_exiftool_num['BrightnessValue'] 99 | assert_equal new_time, @mini_exiftool_num['BrightnessValue'] 100 | end 101 | 102 | def test_integer_conversion 103 | assert_kind_of Integer, @mini_exiftool_num['MeteringMode'] 104 | new_mode = @mini_exiftool_num['MeteringMode'] - 1 105 | @mini_exiftool_num['MeteringMode'] = new_mode 106 | assert_equal new_mode, @mini_exiftool_num['MeteringMode'] 107 | assert @mini_exiftool_num.changed_tags.include?('MeteringMode') 108 | @mini_exiftool_num.save 109 | assert_equal new_mode, @mini_exiftool_num['MeteringMode'] 110 | end 111 | 112 | def test_rational_conversion 113 | new_exposure_time = Rational(1, 125) 114 | @mini_exiftool.exposure_time = new_exposure_time 115 | assert @mini_exiftool.changed?, 'No changing of value.' 116 | ok = @mini_exiftool.save 117 | assert ok, 'Saving failed.' 118 | @mini_exiftool.reload 119 | assert_equal new_exposure_time, @mini_exiftool.exposure_time 120 | end 121 | 122 | def test_list_conversion 123 | arr = ['a', 'b', 'c'] 124 | @mini_exiftool['Keywords'] = arr 125 | ok = @mini_exiftool.save 126 | assert ok 127 | assert_equal arr, @mini_exiftool['Keywords'] 128 | arr = ['text, with', 'commas, let us look'] 129 | @mini_exiftool['Keywords'] = arr 130 | ok = @mini_exiftool.save 131 | assert ok 132 | if MiniExiftool.exiftool_version.to_f < 7.41 133 | assert_equal ['text', 'with', 'commas', 'let us look'], @mini_exiftool['Keywords'] 134 | else 135 | assert_equal arr, @mini_exiftool['Keywords'] 136 | end 137 | end 138 | 139 | def test_revert_one 140 | @mini_exiftool_num['Orientation'] = 2 141 | @mini_exiftool_num['ISO'] = 200 142 | res = @mini_exiftool_num.revert 'Orientation' 143 | assert_equal 1, @mini_exiftool_num['Orientation'] 144 | assert_equal 200, @mini_exiftool_num['ISO'] 145 | assert_equal true, res 146 | res = @mini_exiftool_num.revert 'Orientation' 147 | assert_equal false, res 148 | end 149 | 150 | def test_revert_all 151 | @mini_exiftool_num['Orientation'] = 2 152 | @mini_exiftool_num['ISO'] = 200 153 | res = @mini_exiftool_num.revert 154 | assert_equal 1, @mini_exiftool_num['Orientation'] 155 | assert_equal 400, @mini_exiftool_num['ISO'] 156 | assert_equal true, res 157 | res = @mini_exiftool_num.revert 158 | assert_equal false, res 159 | end 160 | 161 | end 162 | -------------------------------------------------------------------------------- /Tutorial.rdoc: -------------------------------------------------------------------------------- 1 | = Mini Tutorial 2 | 3 | 4 | == Installation 5 | 6 | * Installing the ExifTool command-line application from Phil Harvey 7 | (see http://www.sno.phy.queensu.ca/~phil/exiftool/install.html) 8 | * Installing the Ruby library (gem install mini_exiftool) 9 | 10 | 11 | == Lesson 1: Reading Meta Data 12 | 13 | === A Simple Example 14 | 15 | require 'mini_exiftool' 16 | 17 | photo = MiniExiftool.new 'photo.jpg' 18 | puts photo['DateTimeOriginal'] 19 | 20 | === Smart Tag Names 21 | In the example above we use photo['DateTimeOriginal'] to 22 | get the value for the time the photo was taken. But tag names are not 23 | case sensitive and additional underlines are also irrelevant. So 24 | following expressions are equivalent: 25 | photo['DateTimeOriginal'] 26 | photo['datetimeoriginal'] 27 | photo['date_time_original'] 28 | 29 | It is also possible to use symbols: 30 | photo[:DateTimeOriginal] 31 | photo[:datetimeoriginal] 32 | photo[:date_time_original] 33 | 34 | === Nicer Access Via Dynamic Methods 35 | 36 | Using the []-method is the safest way to access to values of tags 37 | (e. g. Self-timer you can only access this way) but the smarter way is 38 | using dynamic method access. You can write: 39 | photo.datetimeoriginal 40 | or also 41 | photo.date_time_original 42 | 43 | === Value Types 44 | 45 | Following types of values are at the moment supported: 46 | * Array (e. g. Keywords => ['tree', 'gras']) 47 | * Integer (e. g. ISO => 400) 48 | * Float (e. g. FNumber => 9.5) 49 | * String (e. g. Model => DYNAX 7D) 50 | * Time (e. g. DateTimeOriginal => 2005:09:13 20:08:50) 51 | 52 | Be aware, if there is only one value in a tag which can hold multiple 53 | values the result isn't an array! But you can get one with the Array 54 | method: 55 | # only _one_ keyword 56 | p1 = MiniExiftool.new 'p1.jpg' 57 | p1.keywords # => 'red' 58 | # _more than one_ keywords 59 | p3 = MiniExiftool.new 'p3.jpg' 60 | p3.keywords # => ['red', 'yellow', 'green'] 61 | 62 | # if we want to get an array in both cases and don't know 63 | # if there is one ore more values set let's take Array() 64 | Array(p1.keywords) # => ['red'] 65 | Array(p3.keywords) # => ['red', 'yellow', 'green'] 66 | 67 | === Using options 68 | 69 | The ExifTool command-line application has an option (-n) to get values 70 | as numbers if possible, in MiniExiftool you can do this with setting 71 | the :numerical option to +true+ while generating a new 72 | instance with new or using the numerical=-method 73 | combining with calling reload. 74 | 75 | Let's look at an example: 76 | # standard: numerical is false 77 | photo = MiniExiftool.new 'photo.jpg' 78 | photo.exposure_time # => '1/60' (String) 79 | # now with numerical is true 80 | photo.numerical = true 81 | photo.reload 82 | photo.exposure_time # => 0.01666667 (Float) 83 | This behaviour can be useful if you want to do calculations on the 84 | value, if you only want to show the value the standard behaviour is 85 | maybe better. 86 | 87 | The Time class of Ruby cannot handle timestamps before 1st January 1970 88 | on some platforms. If there are timestamps in files before this date it 89 | will result in an error. In this case we can set the option 90 | :timestamps to +DateTime+ to use DateTime objects instead 91 | of Time objects. 92 | 93 | There is another option :composite. If this is set to 94 | +false+ the composite tags are not calculated by the exiftool 95 | command-line application (option -e). 96 | 97 | Further options are 98 | * :ignore_minor_errors to ignore minor 99 | errors (See -m-option of the exiftool command-line application, 100 | default is +false+) 101 | * :coord_format set format for GPS coordinates (See 102 | -c-option of the exiftool command-line application, default is +nil+ 103 | that means exiftool standard) 104 | * :fast useful when reading JPEGs over a slow network connection 105 | (See -fast-option of the exiftool command-line application, default is +false+) 106 | * :fast2 useful when reading JPEGs over a slow network connection 107 | (See -fast2-option of the exiftool command-line application, default is +false+) 108 | * :replace_invalid_chars replace string for invalid 109 | UTF-8 characters or +false+ if no replacing should be done, 110 | default is +false+ 111 | * :exif_encoding, :iptc_encoding, 112 | :xmp_encoding, :png_encoding, 113 | :id3_encoding, :pdf_encoding, 114 | :photoshop_encoding, :quicktime_encoding, 115 | :aiff_encoding, :mie_encoding, 116 | :vorbis_encoding to set this specific encoding (see 117 | -charset option of the exiftool command-line application, default is 118 | +nil+: no encoding specified) 119 | 120 | === Using an IO instance 121 | 122 | require 'mini_exiftool' 123 | require 'open3' 124 | 125 | # Using external curl command 126 | input, output = Open3.popen2("curl -s http://www.url.of.a.photo") 127 | input.close 128 | photo = MiniExiftool.new output 129 | puts photo['ISO'] 130 | 131 | The kind of the parameter +filename_or_io+ is determined via duck typing: 132 | if the argument responds to +to_str+ it is interpreted as filename, if it 133 | responds to +read+ it is interpreted es IO instance. 134 | Attention: If you use an IO instance then writing of values is not supported! 135 | 136 | Look at the show_speedup_with_fast_option example in the MiniExiftool examples 137 | directory for more details about using an IO instance. 138 | 139 | 140 | == Lesson 2: Writing Meta Data 141 | 142 | === Also A Very Simple Example 143 | 144 | require 'mini_exiftool' 145 | 146 | photo = MiniExiftool.new 'photo.jpg' 147 | photo.comment = 'hello world' 148 | photo.save 149 | 150 | 151 | === Save Is Atomar 152 | 153 | If you have changed several values and call the +save+-method either 154 | all changes will be written to the file or nothing. The return value 155 | of the +save+-method is +true+ if all values are written to the file 156 | otherwise save returns +false+. In the last case you can use the 157 | +errors+-method which returns a hash of the tags which values couldn't 158 | be written with an error message for each of them. 159 | 160 | 161 | === Interesting Methods 162 | 163 | Have a look at the changed?-method for checking if the 164 | value of a specific tag is changed or a changing in general is 165 | done. In the same way the +revert+-method reverts the value of a 166 | specific tag or in general all changes. 167 | 168 | You should also look at the rdoc information of MiniExiftool. 169 | 170 | 171 | == Lesson 3: Copying Meta Data 172 | 173 | === Examples 174 | 175 | require 'mini_exiftool' 176 | 177 | photo = MiniExiftool.new('photo.jpg') 178 | 179 | # Update the author tag of photo.jpg with the value of the author tag 180 | # of another_photo.jpg 181 | photo.copy_tags_from('another_photo.jpg', 'Author') 182 | 183 | # It's also possible to use symbols and case is also not meaningful 184 | photo.copy_tags_from('another_photo.jpg', :author) 185 | 186 | # Further more than one tag can be copied at once 187 | photo.copy_tags_from('another_photo', %w[author copyright]) 188 | 189 | Look at the file copy_icc_profile.rb in the examples folder of MiniExiftool. 190 | 191 | 192 | == Further Examples 193 | 194 | Have a look in the examples folder of MiniExiftool. 195 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | 2.8.0 2 | - MiniExiftool doesn't close any longer a given IO to MiniExiftool.new 3 | respectively MiniExiftool#load. You are responsible to do that. 4 | Thanks to mfo for the suggestion and implementing it. 5 | 6 | 2.7.6 7 | - Add Gemfile (for those who use bundler). 8 | 9 | 2.7.5 10 | - Add gem description and fix typo in summary. 11 | 12 | 2.7.4 13 | - Print messages at $stdout when generating the PStore file with ExifTool tag names. 14 | Thanks to Joshfindit for the idea. 15 | 16 | 2.7.3 17 | - Update docs. 18 | 19 | 2.7.2 20 | - Include example files in gem. 21 | 22 | 2.7.1 23 | - Improve test for fast options: Ignore other messages from stderr. 24 | - Fix typo in license identifier. 25 | - Improve example show_speedup_with_fast_option. 26 | 27 | 2.7.0 28 | - Use duck typing to determine if filename_or_io is a filename or an IO 29 | instance. 30 | - New option :fast2. 31 | - Add example show_speedup_with_fast_option. 32 | - Update docs. 33 | 34 | 2.6.0 35 | - Support reading from IO instances. 36 | Thanks to Gaelan for the idea. 37 | - New option :fast to increase speed when extracting information from JPEG 38 | images which are piped across a slow network connection. 39 | Thanks to Felipe Cypriano for the idea. 40 | - Refactoring: Use Open3 for all command-line calls. 41 | 42 | 2.5.1 43 | - Add gemspec. 44 | 45 | 2.5.0 46 | - Make the pstore dir customizable: 47 | MiniExiftool.pstore_dir and MiniExiftool.pstore_dir= 48 | Thanks to Shawn Pyle for the idea and a first approach 49 | of implementation. 50 | - Update README. 51 | 52 | 2.4.2 53 | - Bugfix: Don't ignoring *_encoding options when saving. 54 | 55 | 2.4.1 56 | - Handling tag values of the form x/0 correct. 57 | Thanks to Picturelife for a clever solution to solve this. 58 | - Some internal housekeeping. 59 | 60 | 2.4.0 61 | - New method MiniExiftool#copy_tags_from. 62 | Many thanks to cgat for the hint and implementing a first 63 | approach. 64 | - Saver handling of encoding problems. 65 | - Approving documentation. 66 | - Using regtest for integration testing. 67 | - Some internal refactorings. 68 | 69 | 2.3.0 70 | - New options :exif_encoding, :iptc_encodings, 71 | :xmp_encodings etc. to change specific encodings 72 | See -charset option of the exiftool commandline 73 | application 74 | - Some internal improvements 75 | 76 | 2.2.1 77 | - Bugfix: Ignore filename specific tags to avoid encoding 78 | confusions. 79 | 80 | 2.2.0 81 | The Encoding Release 82 | - New option :replace_invalid_chars to handle "bad data" 83 | invalid byte sequences in UTF-8 84 | Thanks to Chris Salzberg (aka shioyama) and 85 | Robert May (aka robotmay) for precious hints 86 | - Support of different encodings for commandline params 87 | and filenames (neccessary to support Windows) 88 | to allow filenames with special chars 89 | Thanks to uwe58 and others for hints 90 | - Doing different commandline escaping for windows and POSIX 91 | systems 92 | Thanks to Michael Dungan for the hint 93 | - Update Tutorial 94 | 95 | 2.1.0 96 | - insert require 'json' 97 | - Drop option :convert_encoding (use Ruby String 98 | methods instead) 99 | - Make the test_access_coordinate work on different 100 | exiftool version. 101 | 102 | 2.0.0 103 | - Drop Ruby 1.8 compatibility. 104 | - Using JSON to parse exiftool output (need 105 | exiftool 7.65 or higher). 106 | 107 | 1.7.0 108 | - Support exiftool -c option for formatting GPS 109 | coordinates. 110 | Thanks to Lee Horrocks for the patch. 111 | - Switching from shellwords to manual escaping. 112 | Hopefully it works now on Windows systems. 113 | Thanks to uwe58 and jpg0 for the hints. 114 | 115 | 1.6.0 116 | - Type conversion in MiniExiftool.from_hash. 117 | Thanks to Ethan Soutar-Rau for the merge request. 118 | - Switching to rim. (No longer troubles with echoe.) 119 | - ExifTool version detection delayed. 120 | Thanks to Sebastian Skałacki for the merge request. 121 | - New method MiniExiftool#save! 122 | Cherry-picked commit from Wil Gieseler. 123 | 124 | 1.5.1 125 | - Make rational values work on Ruby 1.8.7. 126 | 127 | 1.5.0 128 | - Supporting exiftool command-line option -m. 129 | rubyforge request [#29587] 130 | Thanks to Michael Grove for reporting. 131 | - Supporting rational values. 132 | 133 | 1.4.4 134 | - Fix escaping of values for older versions of Shellwords. 135 | 136 | 1.4.3 137 | - Fixing rubyforge bug [#29596] (Quotes in values) 138 | Thanks to Michael Grove for reporting 139 | 140 | 1.4.2 141 | - Add .yardopts file to gem. 142 | 143 | 1.4.1 144 | - Update documentation for using yard. 145 | 146 | 1.4.0 147 | - Allow symbols for tag access with []. 148 | - Refactoring tests. 149 | 150 | 1.3.1 151 | - Remove TestEscapeFilename test and releating test photo 152 | because the latter produces errors on windows systems. 153 | - Version check in prerelease task. 154 | 155 | 1.3.0 156 | - MiniExiftool is now ready for Ruby 1.9 157 | All tests in the test suite pass. :) 158 | 159 | 1.2.2 160 | - Fixing ptore directory naming convention for darwin. 161 | Thanks to Denis Barushev for the hint. 162 | 163 | 1.2.1 164 | - Switching to echoe. 165 | - Update e-mail address. 166 | 167 | 1.2.0 168 | - Fixing time zone handling. 169 | Thanks to ccoenen for the hint. 170 | 171 | 1.1.0 172 | - Escaping filenames in shell commands 173 | Thanks to Michael Hoy for the hint and implementing a patch which was 174 | the base for this fix. 175 | 176 | 1.0.2 177 | - Fixing warings 178 | Thanks to Peter-Hinrich Krogmann for the hint. 179 | 180 | 1.0.1 181 | - Fixing bug [#22726] 182 | Making MiniExiftool::Error public. 183 | Thanks to Mathias Stjernstrom for sending a patch. 184 | 185 | 1.0.0 186 | - Be aware changing in the interface: 187 | - List tags (e.g. Keywords, SupplementalCategories) are now handled as 188 | arrays. 189 | - Tag SubjectLocation is not longer an array value but a string value! 190 | 191 | 0.7.0 192 | - Changing composite behaviour: Composite tags are now included as standard! 193 | - New method MiniExiftool.opts which returns a hash of the standard 194 | options used for MiniExiftool.new 195 | - New option :convert_encoding for MiniExiftool.new which uses the -L-option 196 | of the exiftool command-line application (see online documentation for it) 197 | Thanks to Henning Kulander for the causing of this change. 198 | 199 | 0.6.0 200 | - New methods for serialization: 201 | - MiniExiftool.from_hash 202 | - MiniExiftool.from_yaml 203 | - MiniExiftool#to_hash 204 | - MiniExiftool#to_yaml 205 | Thanks to Andrew Bennett for the initial idea of YAML-serialization 206 | - Refactoring of tests 207 | - Small documentation update 208 | 209 | 0.5.1 210 | - Warning "parenthesize argument(s) for future version" removed 211 | Thanks to Greg from knobby.ws 212 | 213 | 0.5.0 214 | - New option :timestamps to create DateTime objects instead of Time objects 215 | for timestamps (Fixing bug #16328) 216 | - Invalid values of timestamps (i.e. 0000:00:00 00:00:00) are now mapped 217 | to false 218 | 219 | 0.4.1 220 | - Compatibility for Ruby 1.9 221 | 222 | 0.4.0 223 | - MiniExiftool::Error inherits now from StandardError 224 | - Alternative installation via setup.rb 225 | - Bugfix 226 | Saving of non-read tags doesn't work with tags with hyphen 227 | Thanks to Robin Romahn for reporting the bug 228 | - New methods: MiniExiftool.all_tags and MiniExiftool.original_tag 229 | - Internal: Original tag names (all and writable) are now saved via pstore in 230 | a file for better performance 231 | 232 | 0.3.1 233 | - Typos fixed 234 | 235 | 0.3.0 236 | - Documentation completed and a Mini Tutorial added 237 | - Interface changes: 238 | - Test if a value for a tag can be saved is now done in 239 | MiniExiftool#save 240 | => There is no check at the moment you set a value: 241 | the tag occurs in MiniExiftool#changed_values 242 | => While calling MiniExiftool#save errors can occur (see next point) 243 | - MiniExiftool#save is a transaction: if one or more error occurs the file is 244 | not changed! In such a case the errors can be found in MiniExiftool#errors 245 | - Parameter opts of MiniExiftool.initialize is now a Hash with two options: 246 | - :numerical => read metadata as numerical values 247 | - :composite => read also composite tags 248 | - Tests added 249 | 250 | 0.2.0 251 | - Better error handling (i.e. error messages) 252 | - Checking if the exiftool command can be executed at loading the lib 253 | - New class method exiftool_version 254 | - Added tests 255 | - Documentation completed 256 | 257 | 0.1.2 258 | - Bugfix for Windows (Tempfile) 259 | Thanks to Jérome Soika for testing 260 | - Regexes optimized (a little bit) 261 | - New class-method MiniExiftool.writable_tags 262 | 263 | 0.1.1 264 | - Fixing bug [#8073] 265 | Handling the '-' in tag Self-timer 266 | Thanks to Eric Young 267 | 268 | 0.1.0 269 | - New method "revert" 270 | - More tests 271 | 272 | 0.0.1 273 | - Initial release 274 | -------------------------------------------------------------------------------- /regtest/read_all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sample: read Bad_PreviewIFD.jpg 3 | result: 4 | ExifToolVersion: 10.09 5 | FileSize: 8.4 kB 6 | FileType: JPEG 7 | FileTypeExtension: jpg 8 | MIMEType: image/jpeg 9 | JFIFVersion: 1.01 10 | ExifByteOrder: Big-endian (Motorola, MM) 11 | Make: NIKON CORPORATION 12 | Model: NIKON D60 13 | XResolution: 300 14 | YResolution: 300 15 | ResolutionUnit: inches 16 | Software: GIMP 2.6.11 17 | ModifyDate: 2012-05-31 21:33:12.000000000 +02:00 18 | YCbCrPositioning: Centered 19 | ExposureTime: !ruby/object:Rational 20 | denominator: 125 21 | numerator: 1 22 | FNumber: 5.6 23 | ExposureProgram: Not Defined 24 | ISO: 110 25 | ExifVersion: '0221' 26 | DateTimeOriginal: 2008-12-12 11:12:30.000000000 +01:00 27 | CreateDate: 2008-12-12 11:12:30.000000000 +01:00 28 | ComponentsConfiguration: Y, Cb, Cr, - 29 | ExposureCompensation: 0 30 | MaxApertureValue: 4.0 31 | MeteringMode: Multi-segment 32 | Flash: No Flash 33 | FocalLength: 55.0 mm 34 | MakerNoteVersion: 2.1 35 | ColorMode: Color 36 | Quality: Fine 37 | WhiteBalance: Auto 38 | Sharpness: Auto 39 | FocusMode: AF-A 40 | WhiteBalanceFineTune: 0 0 41 | WB_RBLevels: 56833 21761 1 1 42 | ProgramShift: 0 43 | ExposureDifference: 0 44 | Warning: Bad PreviewIFD directory 45 | ISOSetting: 110 46 | ImageBoundary: 0 0 3872 2592 47 | CropHiSpeed: Off (3904x2616 cropped to 3904x2616 at pixel 0,0) 48 | SerialNumber: 3365614 49 | VRInfoVersion: 100 50 | VibrationReduction: 'On' 51 | VRMode: Normal 52 | ActiveD-Lighting: 'Off' 53 | TimeZone: "-05:00" 54 | DaylightSavings: 'No' 55 | DateDisplayFormat: Y/M/D 56 | ISOExpansion: 'Off' 57 | ISO2: 112 58 | ISOExpansion2: 'Off' 59 | ToneComp: Auto 60 | LensType: G VR 61 | Lens: 55-200mm f/4-5.6 62 | FlashMode: Did Not Fire 63 | AFAreaMode: Dynamic Area (closest subject) 64 | AFPoint: Center 65 | AFPointsInFocus: Center 66 | ShootingMode: Single-Frame 67 | ColorHue: Mode3a 68 | LightSource: Natural 69 | ShotInfoVersion: '0211' 70 | HueAdjustment: 0 71 | NoiseReduction: 'Off' 72 | WB_RGGBLevels: 56833 1 1 7 73 | LensDataVersion: 202 74 | ExitPupilPosition: 70.6 mm 75 | AFAperture: 4.1 76 | FocusPosition: '0x22' 77 | FocusDistance: 6.31 m 78 | LensIDNumber: 144 79 | LensFStops: 4.92 80 | MinFocalLength: 55.0 mm 81 | MaxFocalLength: 201.6 mm 82 | MaxApertureAtMinFocal: 4.0 83 | MaxApertureAtMaxFocal: 5.7 84 | MCUVersion: 146 85 | EffectiveMaxAperture: 4.0 86 | SensorPixelSize: 6.05 x 6.05 um 87 | RetouchHistory: None 88 | ImageDataSize: 4725881 89 | ShutterCount: 290 90 | ImageOptimization: '' 91 | Saturation: Auto 92 | VariProgram: Auto 93 | MultiExposureVersion: 100 94 | MultiExposureMode: 'Off' 95 | MultiExposureShots: 0 96 | MultiExposureAutoGain: 'Off' 97 | HighISONoiseReduction: 'Off' 98 | PowerUpTime: 2008-12-12 11:11:24.000000000 +01:00 99 | FileInfoVersion: 100 100 | DirectoryNumber: 100 101 | FileNumber: 0081 102 | RetouchInfoVersion: 100 103 | NEFBitDepth: Unknown (0 0 16723 17225) 104 | UserComment: '' 105 | SubSecTime: 0 106 | SubSecTimeOriginal: 0 107 | SubSecTimeDigitized: 0 108 | FlashpixVersion: 100 109 | ColorSpace: sRGB 110 | ExifImageWidth: 1 111 | ExifImageHeight: 1 112 | SensingMethod: One-chip color area 113 | FileSource: Digital Camera 114 | SceneType: Directly photographed 115 | CFAPattern: "[Green,Blue][Red,Green]" 116 | XPTitle: '' 117 | XPKeywords: '' 118 | Compression: JPEG (old-style) 119 | ThumbnailOffset: 3690 120 | ThumbnailLength: 631 121 | RatingPercent: 0 122 | Rating: 0 123 | Title: '' 124 | ProfileCMMType: Lino 125 | ProfileVersion: 2.1.0 126 | ProfileClass: Display Device Profile 127 | ColorSpaceData: 'RGB ' 128 | ProfileConnectionSpace: 'XYZ ' 129 | ProfileDateTime: 1998-02-09 06:49:00.000000000 +01:00 130 | ProfileFileSignature: acsp 131 | PrimaryPlatform: Microsoft Corporation 132 | CMMFlags: Not Embedded, Independent 133 | DeviceManufacturer: 'IEC ' 134 | DeviceModel: sRGB 135 | DeviceAttributes: Reflective, Glossy, Positive, Color 136 | RenderingIntent: Perceptual 137 | ConnectionSpaceIlluminant: 0.9642 1 0.82491 138 | ProfileCreator: 'HP ' 139 | ProfileID: 0 140 | ProfileCopyright: Copyright (c) 1998 Hewlett-Packard Company 141 | ProfileDescription: sRGB IEC61966-2.1 142 | MediaWhitePoint: 0.95045 1 1.08905 143 | MediaBlackPoint: 0 0 0 144 | RedMatrixColumn: 0.43607 0.22249 0.01392 145 | GreenMatrixColumn: 0.38515 0.71687 0.09708 146 | BlueMatrixColumn: 0.14307 0.06061 0.7141 147 | DeviceMfgDesc: IEC http://www.iec.ch 148 | DeviceModelDesc: IEC 61966-2.1 Default RGB colour space - sRGB 149 | ViewingCondDesc: Reference Viewing Condition in IEC61966-2.1 150 | ViewingCondIlluminant: 19.6445 20.3718 16.8089 151 | ViewingCondSurround: 3.92889 4.07439 3.36179 152 | ViewingCondIlluminantType: D50 153 | Luminance: 76.03647 80 87.12462 154 | MeasurementObserver: CIE 1931 155 | MeasurementBacking: 0 0 0 156 | MeasurementGeometry: Unknown 157 | MeasurementFlare: 0.999% 158 | MeasurementIlluminant: D65 159 | Technology: Cathode Ray Tube Display 160 | RedTRC: "(Binary data 2060 bytes, use -b option to extract)" 161 | GreenTRC: "(Binary data 2060 bytes, use -b option to extract)" 162 | BlueTRC: "(Binary data 2060 bytes, use -b option to extract)" 163 | ImageWidth: 1 164 | ImageHeight: 1 165 | EncodingProcess: Baseline DCT, Huffman coding 166 | BitsPerSample: 8 167 | ColorComponents: 3 168 | YCbCrSubSampling: YCbCr4:4:0 (1 2) 169 | Aperture: 5.6 170 | BlueBalance: 7 171 | ImageSize: 1x1 172 | LensID: AF-S DX VR Zoom-Nikkor 55-200mm f/4-5.6G IF-ED 173 | LensSpec: 55-200mm f/4-5.6 G VR 174 | Megapixels: 1.0e-06 175 | RedBalance: 56833 176 | ShutterSpeed: !ruby/object:Rational 177 | denominator: 125 178 | numerator: 1 179 | SubSecCreateDate: 2008-12-12 11:12:30.000000000 +01:00 180 | SubSecDateTimeOriginal: 2008-12-12 11:12:30.000000000 +01:00 181 | SubSecModifyDate: 2012-05-31 21:33:12.000000000 +02:00 182 | ThumbnailImage: "(Binary data 631 bytes, use -b option to extract)" 183 | FocalLength35efl: 55.0 mm 184 | LightValue: 11.8 185 | --- 186 | sample: read Canon.jpg 187 | result: 188 | ExifToolVersion: 10.09 189 | FileSize: 2.6 kB 190 | FileType: JPEG 191 | FileTypeExtension: jpg 192 | MIMEType: image/jpeg 193 | ExifByteOrder: Little-endian (Intel, II) 194 | Make: Canon 195 | Model: Canon EOS DIGITAL REBEL 196 | Orientation: Horizontal (normal) 197 | XResolution: 180 198 | YResolution: 180 199 | ResolutionUnit: inches 200 | ModifyDate: 2003-12-04 06:46:52.000000000 +01:00 201 | YCbCrPositioning: Centered 202 | ExposureTime: 4 203 | FNumber: 14.0 204 | ISO: 100 205 | ExifVersion: '0221' 206 | DateTimeOriginal: 2003-12-04 06:46:52.000000000 +01:00 207 | CreateDate: 2003-12-04 06:46:52.000000000 +01:00 208 | ComponentsConfiguration: Y, Cb, Cr, - 209 | CompressedBitsPerPixel: 9 210 | ShutterSpeedValue: 0 211 | ApertureValue: 14.0 212 | MaxApertureValue: 4.5 213 | Flash: No Flash 214 | FocalLength: 34.0 mm 215 | MacroMode: Unknown (0) 216 | SelfTimer: 'Off' 217 | Quality: RAW 218 | CanonFlashMode: 'Off' 219 | ContinuousDrive: Continuous 220 | FocusMode: Manual Focus (3) 221 | RecordMode: CRW+THM 222 | CanonImageSize: Large 223 | EasyMode: Manual 224 | DigitalZoom: Unknown (-1) 225 | Contrast: "+1" 226 | Saturation: "+1" 227 | Sharpness: "+1" 228 | CameraISO: n/a 229 | MeteringMode: Center-weighted average 230 | FocusRange: Not Known 231 | CanonExposureMode: Manual 232 | LensType: n/a 233 | MaxFocalLength: 55 mm 234 | MinFocalLength: 18 mm 235 | FocalUnits: 1/mm 236 | MaxAperture: 4 237 | MinAperture: 27 238 | FlashActivity: 0 239 | FlashBits: "(none)" 240 | ZoomSourceWidth: 3072 241 | ZoomTargetWidth: 3072 242 | ManualFlashOutput: n/a 243 | ColorTone: Normal 244 | FocalPlaneXSize: 23.22 mm 245 | FocalPlaneYSize: 15.49 mm 246 | AutoISO: 100 247 | BaseISO: 100 248 | MeasuredEV: -1.25 249 | TargetAperture: 14 250 | ExposureCompensation: 0 251 | WhiteBalance: Auto 252 | SlowShutter: None 253 | SequenceNumber: 0 254 | OpticalZoomCode: n/a 255 | FlashGuideNumber: 0 256 | FlashExposureComp: 0 257 | AutoExposureBracketing: 'Off' 258 | AEBBracketValue: 0 259 | ControlMode: Camera Local Control 260 | FocusDistanceUpper: inf 261 | FocusDistanceLower: 5.46 m 262 | MeasuredEV2: -1.25 263 | BulbDuration: 4 264 | CameraType: EOS Mid-range 265 | AutoRotate: None 266 | NDFilter: n/a 267 | SelfTimer2: 0 268 | BracketMode: 'Off' 269 | BracketValue: 0 270 | BracketShotNumber: 0 271 | CanonImageType: CRW:EOS DIGITAL REBEL CMOS RAW 272 | CanonFirmwareVersion: Firmware Version 1.1.1 273 | SerialNumber: 560018150 274 | SerialNumberFormat: Format 1 275 | FileNumber: 118-1861 276 | OwnerName: Phil Harvey 277 | CanonModelID: EOS Digital Rebel / 300D / Kiss Digital 278 | CanonFileLength: 4480822 279 | MeasuredRGGB: 998 1022 1026 808 280 | WB_RGGBLevelsAuto: 1719 832 831 990 281 | WB_RGGBLevelsDaylight: 1722 832 831 989 282 | WB_RGGBLevelsShade: 2035 832 831 839 283 | WB_RGGBLevelsCloudy: 1878 832 831 903 284 | WB_RGGBLevelsTungsten: 1228 913 912 1668 285 | WB_RGGBLevelsFluorescent: 1506 842 841 1381 286 | WB_RGGBLevelsFlash: 1933 832 831 895 287 | WB_RGGBLevelsCustom: 1722 832 831 989 288 | WB_RGGBLevelsKelvin: 1722 832 831 988 289 | WB_RGGBBlackLevels: 124 123 124 123 290 | ColorTemperature: 5200 291 | NumAFPoints: 7 292 | ValidAFPoints: 7 293 | CanonImageWidth: 3072 294 | CanonImageHeight: 2048 295 | AFImageWidth: 3072 296 | AFImageHeight: 2048 297 | AFAreaWidth: 151 298 | AFAreaHeight: 151 299 | AFAreaXPositions: 1014 608 0 0 0 -608 -1014 300 | AFAreaYPositions: 0 0 -506 0 506 0 0 301 | AFPointsInFocus: "(none)" 302 | ThumbnailImageValidArea: 0 159 7 112 303 | UserComment: '' 304 | FlashpixVersion: 100 305 | ColorSpace: sRGB 306 | ExifImageWidth: 160 307 | ExifImageHeight: 120 308 | InteropIndex: THM - DCF thumbnail file 309 | InteropVersion: 100 310 | RelatedImageWidth: 3072 311 | RelatedImageHeight: 2048 312 | FocalPlaneXResolution: 3443.946188 313 | FocalPlaneYResolution: 3442.016807 314 | FocalPlaneResolutionUnit: inches 315 | SensingMethod: One-chip color area 316 | FileSource: Digital Camera 317 | CustomRendered: Normal 318 | ExposureMode: Manual 319 | SceneCaptureType: Standard 320 | ImageWidth: 8 321 | ImageHeight: 8 322 | EncodingProcess: Baseline DCT, Huffman coding 323 | BitsPerSample: 8 324 | ColorComponents: 3 325 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 326 | Aperture: 14.0 327 | DriveMode: Continuous Shooting 328 | ImageSize: 8x8 329 | Lens: 18.0 - 55.0 mm 330 | LensID: Unknown 18-55mm 331 | Megapixels: 6.4e-05 332 | ScaleFactor35efl: 1.6 333 | ShootingMode: Bulb 334 | ShutterSpeed: 4 335 | WB_RGGBLevels: 1719 832 831 990 336 | BlueBalance: 1.190619 337 | CircleOfConfusion: 0.019 mm 338 | DOF: inf (4.31 m - inf) 339 | FOV: 36.9 deg 340 | FocalLength35efl: '34.0 mm (35 mm equivalent: 54.0 mm)' 341 | HyperfocalDistance: 4.37 m 342 | Lens35efl: '18.0 - 55.0 mm (35 mm equivalent: 28.6 - 87.4 mm)' 343 | LightValue: 5.6 344 | RedBalance: 2.067348 345 | --- 346 | sample: read test.jpg 347 | result: 348 | ExifToolVersion: 10.09 349 | FileSize: 46 kB 350 | FileType: JPEG 351 | FileTypeExtension: jpg 352 | MIMEType: image/jpeg 353 | JFIFVersion: 1.01 354 | ExifByteOrder: Big-endian (Motorola, MM) 355 | ImageDescription: KONICA MINOLTA DIGITAL CAMERA 356 | Make: KONICA MINOLTA 357 | Model: DYNAX 7D 358 | Orientation: Horizontal (normal) 359 | XResolution: 72 360 | YResolution: 72 361 | ResolutionUnit: inches 362 | Software: DYNAX 7D v1.10 363 | ModifyDate: 2005-09-13 20:08:50.000000000 +02:00 364 | YCbCrPositioning: Centered 365 | ExposureTime: !ruby/object:Rational 366 | denominator: 60 367 | numerator: 1 368 | FNumber: 9.5 369 | ExposureProgram: Program AE 370 | ISO: 400 371 | ExifVersion: '0221' 372 | DateTimeOriginal: 2005-09-13 20:08:50.000000000 +02:00 373 | CreateDate: 2005-09-13 20:08:50.000000000 +02:00 374 | ComponentsConfiguration: Y, Cb, Cr, - 375 | BrightnessValue: 4.5 376 | ExposureCompensation: -1 377 | MaxApertureValue: 4.5 378 | MeteringMode: Multi-segment 379 | LightSource: Unknown 380 | Flash: Off, Did not fire 381 | FocalLength: 75.0 mm 382 | SubjectArea: 1504 1000 256 304 383 | MakerNoteVersion: MLT0 384 | MinoltaImageSize: Large 385 | WhiteBalance: Auto 386 | FocusMode: AF-A 387 | AFPoints: Center 388 | FlashMode: Normal 389 | ISOSetting: 400 390 | FreeMemoryCardImages: 202 391 | HueAdjustment: 0 392 | Rotation: Horizontal (normal) 393 | ImageNumber: 6 394 | NoiseReduction: Unknown (2) 395 | ImageNumber2: 50 396 | ZoneMatchingOn: 'Off' 397 | CompressedImageSize: 1598477 398 | PreviewImageStart: 39152 399 | PreviewImageLength: 0 400 | SceneMode: Standard 401 | ColorMode: Natural sRGB 402 | MinoltaQuality: Fine 403 | FlashExposureComp: 0 404 | Teleconverter: None 405 | ImageStabilization: 'On' 406 | ZoneMatching: ISO Setting Used 407 | ColorTemperature: 0 408 | LensType: Minolta AF 28-135mm F4-4.5 or Sigma Lens 409 | UserComment: '' 410 | FlashpixVersion: 100 411 | ColorSpace: sRGB 412 | ExifImageWidth: 3008 413 | ExifImageHeight: 2000 414 | CustomRendered: Normal 415 | ExposureMode: Auto 416 | DigitalZoomRatio: 0 417 | FocalLengthIn35mmFormat: 112 mm 418 | SceneCaptureType: Standard 419 | GainControl: Low gain up 420 | Contrast: Normal 421 | Saturation: Normal 422 | Sharpness: Normal 423 | PrintIMVersion: 300 424 | Compression: JPEG (old-style) 425 | ThumbnailOffset: 39274 426 | ThumbnailLength: 1820 427 | CurrentIPTCDigest: dd8d51d28ddf04f08f870e5ff2f64d01 428 | Keywords: 429 | - Orange 430 | - Rot 431 | ApplicationRecordVersion: 4 432 | SupplementalCategories: Natur 433 | XMPToolkit: Image::ExifTool 7.03 434 | Title: Abenddämmerung 435 | ImageWidth: 300 436 | ImageHeight: 199 437 | EncodingProcess: Baseline DCT, Huffman coding 438 | BitsPerSample: 8 439 | ColorComponents: 3 440 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 441 | Aperture: 9.5 442 | ImageSize: 300x199 443 | LensID: Minolta AF 28-135mm F4-4.5 444 | Megapixels: 0.06 445 | ScaleFactor35efl: 1.5 446 | ShutterSpeed: !ruby/object:Rational 447 | denominator: 60 448 | numerator: 1 449 | ThumbnailImage: "(Binary data 1820 bytes, use -b option to extract)" 450 | CircleOfConfusion: 0.020 mm 451 | FOV: 18.3 deg 452 | FocalLength35efl: '75.0 mm (35 mm equivalent: 112.0 mm)' 453 | HyperfocalDistance: 29.43 m 454 | LightValue: 10.4 455 | --- 456 | sample: read test_coordinates.jpg 457 | result: 458 | ExifToolVersion: 10.09 459 | FileSize: 251 kB 460 | FileType: JPEG 461 | FileTypeExtension: jpg 462 | MIMEType: image/jpeg 463 | ExifByteOrder: Big-endian (Motorola, MM) 464 | ImageDescription: clip 465 | Orientation: Horizontal (normal) 466 | XResolution: 72 467 | YResolution: 72 468 | ResolutionUnit: inches 469 | YCbCrPositioning: Centered 470 | ExposureTime: !ruby/object:Rational 471 | denominator: 30 472 | numerator: 1 473 | FNumber: 2.4 474 | ExposureProgram: Program AE 475 | ISO: 400 476 | ExifVersion: '0221' 477 | ComponentsConfiguration: Y, Cb, Cr, - 478 | ShutterSpeedValue: !ruby/object:Rational 479 | denominator: 30 480 | numerator: 1 481 | ApertureValue: 2.4 482 | BrightnessValue: 1.954376479 483 | MeteringMode: Multi-segment 484 | Flash: Off, Did not fire 485 | FocalLength: 4.1 mm 486 | SubjectArea: 519 403 239 180 487 | UserComment: clip 488 | FlashpixVersion: 100 489 | ColorSpace: sRGB 490 | ExifImageWidth: 1280 491 | ExifImageHeight: 720 492 | SensingMethod: One-chip color area 493 | ExposureMode: Auto 494 | WhiteBalance: Auto 495 | FocalLengthIn35mmFormat: 35 mm 496 | SceneCaptureType: Standard 497 | GPSLatitudeRef: North 498 | GPSLongitudeRef: West 499 | GPSAltitude: 0 m 500 | GPSTimeStamp: '00:00:00' 501 | GPSDOP: 0 502 | Compression: JPEG (old-style) 503 | ThumbnailOffset: 766 504 | ThumbnailLength: 3831 505 | ImageWidth: 1280 506 | ImageHeight: 720 507 | EncodingProcess: Baseline DCT, Huffman coding 508 | BitsPerSample: 8 509 | ColorComponents: 3 510 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 511 | Aperture: 2.4 512 | GPSLatitude: 43 deg 39' 11.40" N 513 | GPSLongitude: 79 deg 22' 23.40" W 514 | GPSPosition: 43 deg 39' 11.40" N, 79 deg 22' 23.40" W 515 | ImageSize: 1280x720 516 | Megapixels: 0.922 517 | ScaleFactor35efl: 8.5 518 | ShutterSpeed: !ruby/object:Rational 519 | denominator: 30 520 | numerator: 1 521 | ThumbnailImage: "(Binary data 3831 bytes, use -b option to extract)" 522 | CircleOfConfusion: 0.004 mm 523 | FOV: 54.4 deg 524 | FocalLength35efl: '4.1 mm (35 mm equivalent: 35.0 mm)' 525 | HyperfocalDistance: 2.00 m 526 | LightValue: 5.4 527 | --- 528 | sample: read test_encodings.jpg 529 | result: 530 | ExifToolVersion: 10.09 531 | FileSize: 660 bytes 532 | FileType: JPEG 533 | FileTypeExtension: jpg 534 | MIMEType: image/jpeg 535 | CurrentIPTCDigest: 09b79ab4703f3570ba3140ffc2dba2f6 536 | ObjectName: Mšhre 537 | ApplicationRecordVersion: 4 538 | ImageWidth: 1 539 | ImageHeight: 1 540 | EncodingProcess: Baseline DCT, Huffman coding 541 | BitsPerSample: 8 542 | ColorComponents: 3 543 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 544 | ImageSize: 1x1 545 | Megapixels: 1.0e-06 546 | --- 547 | sample: read test_special_dates.jpg 548 | result: 549 | ExifToolVersion: 10.09 550 | FileSize: 3.7 kB 551 | FileType: JPEG 552 | FileTypeExtension: jpg 553 | MIMEType: image/jpeg 554 | ExifByteOrder: Big-endian (Motorola, MM) 555 | XResolution: 72 556 | YResolution: 72 557 | ResolutionUnit: inches 558 | ModifyDate: false 559 | YCbCrPositioning: Centered 560 | ExifVersion: 220 561 | DateTimeOriginal: 1961-08-13 12:08:25.000000000 +01:00 562 | ComponentsConfiguration: Y, Cb, Cr, - 563 | FlashpixVersion: 100 564 | ColorSpace: Uncalibrated 565 | PreviewDateTime: 1961-08-13 12:08:25.000000000 +01:00 566 | ImageWidth: 300 567 | ImageHeight: 199 568 | EncodingProcess: Baseline DCT, Huffman coding 569 | BitsPerSample: 8 570 | ColorComponents: 3 571 | YCbCrSubSampling: YCbCr4:2:0 (2 2) 572 | ImageSize: 300x199 573 | Megapixels: 0.06 574 | -------------------------------------------------------------------------------- /lib/mini_exiftool.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | # 3 | # MiniExiftool 4 | # 5 | # This library is wrapper for the ExifTool command-line 6 | # application (http://www.sno.phy.queensu.ca/~phil/exiftool/) 7 | # written by Phil Harvey. 8 | # Read and write access is done in a clean OO manner. 9 | # 10 | # Author: Jan Friedrich 11 | # Copyright (c) 2007-2016 by Jan Friedrich 12 | # Licensed under the GNU LESSER GENERAL PUBLIC LICENSE, 13 | # Version 2.1, February 1999 14 | # 15 | 16 | require 'fileutils' 17 | require 'json' 18 | require 'open3' 19 | require 'pstore' 20 | require 'rational' 21 | require 'rbconfig' 22 | require 'set' 23 | require 'tempfile' 24 | require 'time' 25 | 26 | # Simple OO access to the ExifTool command-line application. 27 | class MiniExiftool 28 | 29 | VERSION = '2.8.0' 30 | 31 | # Name of the ExifTool command-line application 32 | @@cmd = 'exiftool' 33 | 34 | # Hash of the standard options used when call MiniExiftool.new 35 | @@opts = { :numerical => false, :composite => true, :fast => false, :fast2 => false, 36 | :ignore_minor_errors => false, :replace_invalid_chars => false, 37 | :timestamps => Time } 38 | 39 | # Encoding of the filesystem (filenames in command line) 40 | @@fs_enc = Encoding.find('filesystem') 41 | 42 | def self.opts_accessor *attrs 43 | attrs.each do |a| 44 | define_method a do 45 | @opts[a] 46 | end 47 | define_method "#{a}=" do |val| 48 | @opts[a] = val 49 | end 50 | end 51 | end 52 | 53 | attr_reader :filename, :errors, :io 54 | 55 | opts_accessor :numerical, :composite, :ignore_minor_errors, 56 | :replace_invalid_chars, :timestamps 57 | 58 | @@encoding_types = %w(exif iptc xmp png id3 pdf photoshop quicktime aiff mie vorbis) 59 | 60 | def self.encoding_opt enc_type 61 | (enc_type.to_s + '_encoding').to_sym 62 | end 63 | 64 | @@encoding_types.each do |enc_type| 65 | opts_accessor encoding_opt(enc_type) 66 | end 67 | 68 | # +filename_or_io+ The kind of the parameter is determined via duck typing: 69 | # if the argument responds to +to_str+ it is interpreted as filename, if it 70 | # responds to +read+ it is interpreted es IO instance. 71 | # 72 | # ATTENTION: If using an IO instance writing of meta data is not supported! 73 | # 74 | # +opts+ support at the moment 75 | # * :numerical for numerical values, default is +false+ 76 | # * :composite for including composite tags while loading, 77 | # default is +true+ 78 | # * :ignore_minor_errors ignore minor errors (See -m-option 79 | # of the exiftool command-line application, default is +false+) 80 | # * :coord_format set format for GPS coordinates (See 81 | # -c-option of the exiftool command-line application, default is +nil+ 82 | # that means exiftool standard) 83 | # * :fast useful when reading JPEGs over a slow network connection 84 | # (See -fast-option of the exiftool command-line application, default is +false+) 85 | # * :fast2 useful when reading JPEGs over a slow network connection 86 | # (See -fast2-option of the exiftool command-line application, default is +false+) 87 | # * :replace_invalid_chars replace string for invalid 88 | # UTF-8 characters or +false+ if no replacing should be done, 89 | # default is +false+ 90 | # * :timestamps generating DateTime objects instead of 91 | # Time objects if set to DateTime, default is +Time+ 92 | # 93 | # ATTENTION: Time objects are created using Time.local 94 | # therefore they use your local timezone, DateTime objects instead 95 | # are created without timezone! 96 | # * :exif_encoding, :iptc_encoding, 97 | # :xmp_encoding, :png_encoding, 98 | # :id3_encoding, :pdf_encoding, 99 | # :photoshop_encoding, :quicktime_encoding, 100 | # :aiff_encoding, :mie_encoding, 101 | # :vorbis_encoding to set this specific encoding (see 102 | # -charset option of the exiftool command-line application, default is 103 | # +nil+: no encoding specified) 104 | def initialize filename_or_io=nil, opts={} 105 | @opts = @@opts.merge opts 106 | if @opts[:convert_encoding] 107 | warn 'Option :convert_encoding is not longer supported!' 108 | warn 'Please use the String#encod* methods.' 109 | end 110 | @filename = nil 111 | @io = nil 112 | @values = TagHash.new 113 | @changed_values = TagHash.new 114 | @errors = TagHash.new 115 | load filename_or_io unless filename_or_io.nil? 116 | end 117 | 118 | def initialize_from_hash hash # :nodoc: 119 | set_values hash 120 | set_opts_by_heuristic 121 | self 122 | end 123 | 124 | def initialize_from_json json # :nodoc: 125 | @output = json 126 | @errors.clear 127 | parse_output 128 | self 129 | end 130 | 131 | # Load the tags of filename or io. 132 | def load filename_or_io 133 | if filename_or_io.respond_to? :to_str # String-like 134 | unless filename_or_io && File.exist?(filename_or_io) 135 | raise MiniExiftool::Error.new("File '#{filename_or_io}' does not exist.") 136 | end 137 | if File.directory?(filename_or_io) 138 | raise MiniExiftool::Error.new("'#{filename_or_io}' is a directory.") 139 | end 140 | @filename = filename_or_io.to_str 141 | elsif filename_or_io.respond_to? :read # IO-like 142 | @io = filename_or_io 143 | @filename = '-' 144 | else 145 | raise MiniExiftool::Error.new("Could not open filename_or_io.") 146 | end 147 | @values.clear 148 | @changed_values.clear 149 | params = '-j ' 150 | params << (@opts[:numerical] ? '-n ' : '') 151 | params << (@opts[:composite] ? '' : '-e ') 152 | params << (@opts[:coord_format] ? "-c \"#{@opts[:coord_format]}\"" : '') 153 | params << (@opts[:fast] ? '-fast ' : '') 154 | params << (@opts[:fast2] ? '-fast2 ' : '') 155 | params << generate_encoding_params 156 | if run(cmd_gen(params, @filename)) 157 | parse_output 158 | else 159 | raise MiniExiftool::Error.new(@error_text) 160 | end 161 | self 162 | end 163 | 164 | # Reload the tags of an already read file. 165 | def reload 166 | load @filename 167 | end 168 | 169 | # Returns the value of a tag. 170 | def [] tag 171 | @changed_values[tag] || @values[tag] 172 | end 173 | 174 | # Set the value of a tag. 175 | def []= tag, val 176 | @changed_values[tag] = val 177 | end 178 | 179 | # Returns true if any tag value is changed or if the value of a 180 | # given tag is changed. 181 | def changed? tag=false 182 | if tag 183 | @changed_values.include? tag 184 | else 185 | !@changed_values.empty? 186 | end 187 | end 188 | 189 | # Revert all changes or the change of a given tag. 190 | def revert tag=nil 191 | if tag 192 | val = @changed_values.delete(tag) 193 | res = val != nil 194 | else 195 | res = @changed_values.size > 0 196 | @changed_values.clear 197 | end 198 | res 199 | end 200 | 201 | # Returns an array of the tags (original tag names) of the read file. 202 | def tags 203 | @values.keys.map { |key| MiniExiftool.original_tag(key) } 204 | end 205 | 206 | # Returns an array of all changed tags. 207 | def changed_tags 208 | @changed_values.keys.map { |key| MiniExiftool.original_tag(key) } 209 | end 210 | 211 | # Save the changes to the file. 212 | def save 213 | if @io 214 | raise MiniExiftool::Error.new('No writing support when using an IO.') 215 | end 216 | return false if @changed_values.empty? 217 | @errors.clear 218 | temp_file = Tempfile.new('mini_exiftool') 219 | temp_file.close 220 | temp_filename = temp_file.path 221 | FileUtils.cp filename.encode(@@fs_enc), temp_filename 222 | all_ok = true 223 | @changed_values.each do |tag, val| 224 | original_tag = MiniExiftool.original_tag(tag) 225 | arr_val = val.kind_of?(Array) ? val : [val] 226 | arr_val.map! {|e| convert_before_save(e)} 227 | params = '-q -P -overwrite_original ' 228 | params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '') 229 | params << (@opts[:ignore_minor_errors] ? '-m ' : '') 230 | params << generate_encoding_params 231 | arr_val.each do |v| 232 | params << %Q(-#{original_tag}=#{escape(v)} ) 233 | end 234 | result = run(cmd_gen(params, temp_filename)) 235 | unless result 236 | all_ok = false 237 | @errors[tag] = @error_text.gsub(/Nothing to do.\n\z/, '').chomp 238 | end 239 | end 240 | if all_ok 241 | FileUtils.cp temp_filename, filename.encode(@@fs_enc) 242 | reload 243 | end 244 | temp_file.delete 245 | all_ok 246 | end 247 | 248 | def save! 249 | unless save 250 | err = [] 251 | @errors.each do |key, value| 252 | err << "(#{key}) #{value}" 253 | end 254 | raise MiniExiftool::Error.new("MiniExiftool couldn't save. The following errors occurred: #{err.empty? ? "None" : err.join(", ")}") 255 | end 256 | end 257 | 258 | def copy_tags_from(source_filename, tags) 259 | @errors.clear 260 | unless File.exist?(source_filename) 261 | raise MiniExiftool::Error.new("Source file #{source_filename} does not exist!") 262 | end 263 | params = '-q -P -overwrite_original ' 264 | tags_params = Array(tags).map {|t| '-' << t.to_s}.join(' ') 265 | cmd = [@@cmd, params, '-tagsFromFile', escape(source_filename).encode(@@fs_enc), tags_params.encode('UTF-8'), escape(filename).encode(@@fs_enc)].join(' ') 266 | cmd.force_encoding('UTF-8') 267 | result = run(cmd) 268 | reload 269 | result 270 | end 271 | 272 | # Returns a hash of the original loaded values of the MiniExiftool 273 | # instance. 274 | def to_hash 275 | result = {} 276 | @values.each do |k,v| 277 | result[MiniExiftool.original_tag(k)] = v 278 | end 279 | result 280 | end 281 | 282 | # Returns a YAML representation of the original loaded values of the 283 | # MiniExiftool instance. 284 | def to_yaml 285 | to_hash.to_yaml 286 | end 287 | 288 | # Create a MiniExiftool instance from a hash. Default value 289 | # conversions will be applied if neccesary. 290 | def self.from_hash hash, opts={} 291 | instance = MiniExiftool.new nil, opts 292 | instance.initialize_from_hash hash 293 | instance 294 | end 295 | 296 | # Create a MiniExiftool instance from JSON data. Default value 297 | # conversions will be applied if neccesary. 298 | def self.from_json json, opts={} 299 | instance = MiniExiftool.new nil, opts 300 | instance.initialize_from_json json 301 | instance 302 | end 303 | 304 | # Create a MiniExiftool instance from YAML data created with 305 | # MiniExiftool#to_yaml 306 | def self.from_yaml yaml, opts={} 307 | MiniExiftool.from_hash YAML.load(yaml), opts 308 | end 309 | 310 | # Returns the command name of the called ExifTool application. 311 | def self.command 312 | @@cmd 313 | end 314 | 315 | # Setting the command name of the called ExifTool application. 316 | def self.command= cmd 317 | @@cmd = cmd 318 | end 319 | 320 | # Returns the options hash. 321 | def self.opts 322 | @@opts 323 | end 324 | 325 | # Returns a set of all known tags of ExifTool. 326 | def self.all_tags 327 | unless defined? @@all_tags 328 | @@all_tags = pstore_get :all_tags 329 | end 330 | @@all_tags 331 | end 332 | 333 | # Returns a set of all possible writable tags of ExifTool. 334 | def self.writable_tags 335 | unless defined? @@writable_tags 336 | @@writable_tags = pstore_get :writable_tags 337 | end 338 | @@writable_tags 339 | end 340 | 341 | # Returns the original ExifTool name of the given tag 342 | def self.original_tag tag 343 | unless defined? @@all_tags_map 344 | @@all_tags_map = pstore_get :all_tags_map 345 | end 346 | @@all_tags_map[tag] 347 | end 348 | 349 | # Returns the version of the ExifTool command-line application. 350 | def self.exiftool_version 351 | Open3.popen3 "#{MiniExiftool.command} -ver" do |_inp, out, _err, _thr| 352 | out.read.chomp! 353 | end 354 | rescue SystemCallError 355 | raise MiniExiftool::Error.new("Command '#{MiniExiftool.command}' not found") 356 | end 357 | 358 | def self.unify tag 359 | tag.to_s.gsub(/[-_]/,'').downcase 360 | end 361 | 362 | @@running_on_windows = /mswin|mingw|cygwin/ === RbConfig::CONFIG['host_os'] 363 | 364 | def self.pstore_dir 365 | unless defined? @@pstore_dir 366 | # This will hopefully work on *NIX and Windows systems 367 | home = ENV['HOME'] || ENV['HOMEDRIVE'] + ENV['HOMEPATH'] || ENV['USERPROFILE'] 368 | subdir = @@running_on_windows ? '_mini_exiftool' : '.mini_exiftool' 369 | @@pstore_dir = File.join(home, subdir) 370 | end 371 | @@pstore_dir 372 | end 373 | 374 | def self.pstore_dir= dir 375 | @@pstore_dir = dir 376 | end 377 | 378 | # Exception class 379 | class MiniExiftool::Error < StandardError; end 380 | 381 | ############################################################################ 382 | private 383 | ############################################################################ 384 | 385 | def cmd_gen arg_str='', filename 386 | [@@cmd, arg_str.encode('UTF-8'), escape(filename.encode(@@fs_enc))].map {|s| s.force_encoding('UTF-8')}.join(' ') 387 | end 388 | 389 | def run cmd 390 | if $DEBUG 391 | $stderr.puts cmd 392 | end 393 | status = Open3.popen3(cmd) do |inp, out, err, thr| 394 | if @io 395 | begin 396 | IO.copy_stream @io, inp 397 | rescue Errno::EPIPE 398 | # Output closed, no problem 399 | rescue ::IOError => e 400 | raise MiniExiftool::Error.new("IO is not readable.") 401 | end 402 | inp.close 403 | end 404 | @output = out.read 405 | @error_text = err.read 406 | thr.value.exitstatus 407 | end 408 | status == 0 409 | end 410 | 411 | def convert_before_save val 412 | case val 413 | when Time 414 | val = val.strftime('%Y:%m:%d %H:%M:%S') 415 | end 416 | val 417 | end 418 | 419 | def method_missing symbol, *args 420 | tag_name = symbol.id2name 421 | if tag_name.sub!(/=$/, '') 422 | self[tag_name] = args.first 423 | else 424 | self[tag_name] 425 | end 426 | end 427 | 428 | def parse_output 429 | adapt_encoding 430 | set_values JSON.parse(@output).first 431 | end 432 | 433 | def adapt_encoding 434 | @output.force_encoding('UTF-8') 435 | if @opts[:replace_invalid_chars] && !@output.valid_encoding? 436 | @output.encode!('UTF-16le', invalid: :replace, replace: @opts[:replace_invalid_chars]).encode!('UTF-8') 437 | end 438 | end 439 | 440 | def convert_after_load tag, value 441 | return value unless value.kind_of?(String) 442 | return value unless value.valid_encoding? 443 | case value 444 | when /^\d{4}:\d\d:\d\d \d\d:\d\d:\d\d/ 445 | s = value.sub(/^(\d+):(\d+):/, '\1-\2-') 446 | begin 447 | if @opts[:timestamps] == Time 448 | value = Time.parse(s) 449 | elsif @opts[:timestamps] == DateTime 450 | value = DateTime.parse(s) 451 | else 452 | raise MiniExiftool::Error.new("Value #{@opts[:timestamps]} not allowed for option timestamps.") 453 | end 454 | rescue ArgumentError 455 | value = false 456 | end 457 | when /^\+\d+\.\d+$/ 458 | value = value.to_f 459 | when /^0+[1-9]+$/ 460 | # nothing => String 461 | when /^-?\d+$/ 462 | value = value.to_i 463 | when %r(^(\d+)/(\d+)$) 464 | value = Rational($1.to_i, $2.to_i) rescue value 465 | when /^[\d ]+$/ 466 | # nothing => String 467 | end 468 | value 469 | end 470 | 471 | def set_values hash 472 | hash.each_pair do |tag,val| 473 | @values[tag] = convert_after_load(tag, val) 474 | end 475 | # Remove filename specific tags use attr_reader 476 | # MiniExiftool#filename instead 477 | # Cause: value of tag filename and attribute 478 | # filename have different content, the latter 479 | # holds the filename with full path (like the 480 | # sourcefile tag) and the former the basename 481 | # of the filename also there is no official 482 | # "original tag name" for sourcefile 483 | %w(directory filename sourcefile).each do |t| 484 | @values.delete(t) 485 | end 486 | end 487 | 488 | def set_opts_by_heuristic 489 | @opts[:composite] = tags.include?('ImageSize') 490 | @opts[:numerical] = self.file_size.kind_of?(Integer) 491 | @opts[:timestamps] = self.FileModifyDate.kind_of?(DateTime) ? DateTime : Time 492 | end 493 | 494 | def self.pstore_get attribute 495 | load_or_create_pstore unless defined? @@pstore 496 | result = nil 497 | @@pstore.transaction(true) do |ps| 498 | result = ps[attribute] 499 | end 500 | result 501 | end 502 | 503 | def self.load_or_create_pstore 504 | FileUtils.mkdir_p(pstore_dir) 505 | pstore_filename = File.join(pstore_dir, 'exiftool_tags_' << exiftool_version.gsub('.', '_') << '.pstore') 506 | @@pstore = PStore.new pstore_filename 507 | if !File.exist?(pstore_filename) || File.size(pstore_filename) == 0 508 | $stderr.puts 'Generating cache file for ExifTool tag names. This takes a few seconds but is only needed once...' 509 | @@pstore.transaction do |ps| 510 | ps[:all_tags] = all_tags = determine_tags('list') 511 | ps[:writable_tags] = determine_tags('listw') 512 | map = {} 513 | all_tags.each { |k| map[unify(k)] = k } 514 | ps[:all_tags_map] = map 515 | end 516 | $stderr.puts 'Cache file generated.' 517 | end 518 | end 519 | 520 | def self.determine_tags arg 521 | output = `#{@@cmd} -#{arg}` 522 | lines = output.split(/\n/) 523 | tags = Set.new 524 | lines.each do |line| 525 | next unless line =~ /^\s/ 526 | tags |= line.chomp.split 527 | end 528 | tags 529 | end 530 | 531 | if @@running_on_windows 532 | def escape val 533 | '"' << val.to_s.gsub(/([\\"])/, "\\\\\\1") << '"' 534 | end 535 | else 536 | def escape val 537 | '"' << val.to_s.gsub(/([\\"$])/, "\\\\\\1") << '"' 538 | end 539 | end 540 | 541 | def generate_encoding_params 542 | params = '' 543 | @@encoding_types.each do |enc_type| 544 | if enc_val = @opts[MiniExiftool.encoding_opt(enc_type)] 545 | params << "-charset #{enc_type}=#{enc_val} " 546 | end 547 | end 548 | params 549 | end 550 | 551 | # Hash with indifferent access: 552 | # DateTimeOriginal == datetimeoriginal == date_time_original 553 | class TagHash < Hash # :nodoc: 554 | def[] k 555 | super(unify(k)) 556 | end 557 | def []= k, v 558 | super(unify(k), v) 559 | end 560 | def delete k 561 | super(unify(k)) 562 | end 563 | 564 | def unify tag 565 | MiniExiftool.unify tag 566 | end 567 | end 568 | 569 | end 570 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /setup.rb: -------------------------------------------------------------------------------- 1 | # -- encoding: utf-8 -- 2 | # 3 | # setup.rb 4 | # 5 | # Copyright (c) 2000-2005 Minero Aoki 6 | # 7 | # This program is free software. 8 | # You can distribute/modify this program under the terms of 9 | # the GNU LGPL, Lesser General Public License version 2.1. 10 | # 11 | 12 | unless Enumerable.method_defined?(:map) # Ruby 1.4.6 13 | module Enumerable 14 | alias map collect 15 | end 16 | end 17 | 18 | unless File.respond_to?(:read) # Ruby 1.6 19 | def File.read(fname) 20 | open(fname) {|f| 21 | return f.read 22 | } 23 | end 24 | end 25 | 26 | unless Errno.const_defined?(:ENOTEMPTY) # Windows? 27 | module Errno 28 | class ENOTEMPTY 29 | # We do not raise this exception, implementation is not needed. 30 | end 31 | end 32 | end 33 | 34 | def File.binread(fname) 35 | open(fname, 'rb') {|f| 36 | return f.read 37 | } 38 | end 39 | 40 | # for corrupted Windows' stat(2) 41 | def File.dir?(path) 42 | File.directory?((path[-1,1] == '/') ? path : path + '/') 43 | end 44 | 45 | 46 | class ConfigTable 47 | 48 | include Enumerable 49 | 50 | def initialize(rbconfig) 51 | @rbconfig = rbconfig 52 | @items = [] 53 | @table = {} 54 | # options 55 | @install_prefix = nil 56 | @config_opt = nil 57 | @verbose = true 58 | @no_harm = false 59 | end 60 | 61 | attr_accessor :install_prefix 62 | attr_accessor :config_opt 63 | 64 | attr_writer :verbose 65 | 66 | def verbose? 67 | @verbose 68 | end 69 | 70 | attr_writer :no_harm 71 | 72 | def no_harm? 73 | @no_harm 74 | end 75 | 76 | def [](key) 77 | lookup(key).resolve(self) 78 | end 79 | 80 | def []=(key, val) 81 | lookup(key).set val 82 | end 83 | 84 | def names 85 | @items.map {|i| i.name } 86 | end 87 | 88 | def each(&block) 89 | @items.each(&block) 90 | end 91 | 92 | def key?(name) 93 | @table.key?(name) 94 | end 95 | 96 | def lookup(name) 97 | @table[name] or setup_rb_error "no such config item: #{name}" 98 | end 99 | 100 | def add(item) 101 | @items.push item 102 | @table[item.name] = item 103 | end 104 | 105 | def remove(name) 106 | item = lookup(name) 107 | @items.delete_if {|i| i.name == name } 108 | @table.delete_if {|name, i| i.name == name } 109 | item 110 | end 111 | 112 | def load_script(path, inst = nil) 113 | if File.file?(path) 114 | MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path 115 | end 116 | end 117 | 118 | def savefile 119 | '.config' 120 | end 121 | 122 | def load_savefile 123 | begin 124 | File.foreach(savefile()) do |line| 125 | k, v = *line.split(/=/, 2) 126 | self[k] = v.strip 127 | end 128 | rescue Errno::ENOENT 129 | setup_rb_error $!.message + "\n#{File.basename($0)} config first" 130 | end 131 | end 132 | 133 | def save 134 | @items.each {|i| i.value } 135 | File.open(savefile(), 'w') {|f| 136 | @items.each do |i| 137 | f.printf "%s=%s\n", i.name, i.value if i.value? and i.value 138 | end 139 | } 140 | end 141 | 142 | def load_standard_entries 143 | standard_entries(@rbconfig).each do |ent| 144 | add ent 145 | end 146 | end 147 | 148 | def standard_entries(rbconfig) 149 | c = rbconfig 150 | 151 | rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) 152 | 153 | major = c['MAJOR'].to_i 154 | minor = c['MINOR'].to_i 155 | teeny = c['TEENY'].to_i 156 | version = "#{major}.#{minor}" 157 | 158 | # ruby ver. >= 1.4.4? 159 | newpath_p = ((major >= 2) or 160 | ((major == 1) and 161 | ((minor >= 5) or 162 | ((minor == 4) and (teeny >= 4))))) 163 | 164 | if c['rubylibdir'] 165 | # V > 1.6.3 166 | libruby = "#{c['prefix']}/lib/ruby" 167 | librubyver = c['rubylibdir'] 168 | librubyverarch = c['archdir'] 169 | siteruby = c['sitedir'] 170 | siterubyver = c['sitelibdir'] 171 | siterubyverarch = c['sitearchdir'] 172 | elsif newpath_p 173 | # 1.4.4 <= V <= 1.6.3 174 | libruby = "#{c['prefix']}/lib/ruby" 175 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 176 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 177 | siteruby = c['sitedir'] 178 | siterubyver = "$siteruby/#{version}" 179 | siterubyverarch = "$siterubyver/#{c['arch']}" 180 | else 181 | # V < 1.4.4 182 | libruby = "#{c['prefix']}/lib/ruby" 183 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 184 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 185 | siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" 186 | siterubyver = siteruby 187 | siterubyverarch = "$siterubyver/#{c['arch']}" 188 | end 189 | parameterize = lambda {|path| 190 | path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') 191 | } 192 | 193 | if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } 194 | makeprog = arg.sub(/'/, '').split(/=/, 2)[1] 195 | else 196 | makeprog = 'make' 197 | end 198 | 199 | [ 200 | ExecItem.new('installdirs', 'std/site/home', 201 | 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ 202 | {|val, table| 203 | case val 204 | when 'std' 205 | table['rbdir'] = '$librubyver' 206 | table['sodir'] = '$librubyverarch' 207 | when 'site' 208 | table['rbdir'] = '$siterubyver' 209 | table['sodir'] = '$siterubyverarch' 210 | when 'home' 211 | setup_rb_error '$HOME was not set' unless ENV['HOME'] 212 | table['prefix'] = ENV['HOME'] 213 | table['rbdir'] = '$libdir/ruby' 214 | table['sodir'] = '$libdir/ruby' 215 | end 216 | }, 217 | PathItem.new('prefix', 'path', c['prefix'], 218 | 'path prefix of target environment'), 219 | PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 220 | 'the directory for commands'), 221 | PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 222 | 'the directory for libraries'), 223 | PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 224 | 'the directory for shared data'), 225 | PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 226 | 'the directory for man pages'), 227 | PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 228 | 'the directory for system configuration files'), 229 | PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 230 | 'the directory for local state data'), 231 | PathItem.new('libruby', 'path', libruby, 232 | 'the directory for ruby libraries'), 233 | PathItem.new('librubyver', 'path', librubyver, 234 | 'the directory for standard ruby libraries'), 235 | PathItem.new('librubyverarch', 'path', librubyverarch, 236 | 'the directory for standard ruby extensions'), 237 | PathItem.new('siteruby', 'path', siteruby, 238 | 'the directory for version-independent aux ruby libraries'), 239 | PathItem.new('siterubyver', 'path', siterubyver, 240 | 'the directory for aux ruby libraries'), 241 | PathItem.new('siterubyverarch', 'path', siterubyverarch, 242 | 'the directory for aux ruby binaries'), 243 | PathItem.new('rbdir', 'path', '$siterubyver', 244 | 'the directory for ruby scripts'), 245 | PathItem.new('sodir', 'path', '$siterubyverarch', 246 | 'the directory for ruby extentions'), 247 | PathItem.new('rubypath', 'path', rubypath, 248 | 'the path to set to #! line'), 249 | ProgramItem.new('rubyprog', 'name', rubypath, 250 | 'the ruby program using for installation'), 251 | ProgramItem.new('makeprog', 'name', makeprog, 252 | 'the make program to compile ruby extentions'), 253 | SelectItem.new('shebang', 'all/ruby/never', 'ruby', 254 | 'shebang line (#!) editing mode'), 255 | BoolItem.new('without-ext', 'yes/no', 'no', 256 | 'does not compile/install ruby extentions') 257 | ] 258 | end 259 | private :standard_entries 260 | 261 | def load_multipackage_entries 262 | multipackage_entries().each do |ent| 263 | add ent 264 | end 265 | end 266 | 267 | def multipackage_entries 268 | [ 269 | PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 270 | 'package names that you want to install'), 271 | PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 272 | 'package names that you do not want to install') 273 | ] 274 | end 275 | private :multipackage_entries 276 | 277 | ALIASES = { 278 | 'std-ruby' => 'librubyver', 279 | 'stdruby' => 'librubyver', 280 | 'rubylibdir' => 'librubyver', 281 | 'archdir' => 'librubyverarch', 282 | 'site-ruby-common' => 'siteruby', # For backward compatibility 283 | 'site-ruby' => 'siterubyver', # For backward compatibility 284 | 'bin-dir' => 'bindir', 285 | 'bin-dir' => 'bindir', 286 | 'rb-dir' => 'rbdir', 287 | 'so-dir' => 'sodir', 288 | 'data-dir' => 'datadir', 289 | 'ruby-path' => 'rubypath', 290 | 'ruby-prog' => 'rubyprog', 291 | 'ruby' => 'rubyprog', 292 | 'make-prog' => 'makeprog', 293 | 'make' => 'makeprog' 294 | } 295 | 296 | def fixup 297 | ALIASES.each do |ali, name| 298 | @table[ali] = @table[name] 299 | end 300 | @items.freeze 301 | @table.freeze 302 | @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ 303 | end 304 | 305 | def parse_opt(opt) 306 | m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" 307 | m.to_a[1,2] 308 | end 309 | 310 | def dllext 311 | @rbconfig['DLEXT'] 312 | end 313 | 314 | def value_config?(name) 315 | lookup(name).value? 316 | end 317 | 318 | class Item 319 | def initialize(name, template, default, desc) 320 | @name = name.freeze 321 | @template = template 322 | @value = default 323 | @default = default 324 | @description = desc 325 | end 326 | 327 | attr_reader :name 328 | attr_reader :description 329 | 330 | attr_accessor :default 331 | alias help_default default 332 | 333 | def help_opt 334 | "--#{@name}=#{@template}" 335 | end 336 | 337 | def value? 338 | true 339 | end 340 | 341 | def value 342 | @value 343 | end 344 | 345 | def resolve(table) 346 | @value.gsub(%r<\$([^/]+)>) { table[$1] } 347 | end 348 | 349 | def set(val) 350 | @value = check(val) 351 | end 352 | 353 | private 354 | 355 | def check(val) 356 | setup_rb_error "config: --#{name} requires argument" unless val 357 | val 358 | end 359 | end 360 | 361 | class BoolItem < Item 362 | def config_type 363 | 'bool' 364 | end 365 | 366 | def help_opt 367 | "--#{@name}" 368 | end 369 | 370 | private 371 | 372 | def check(val) 373 | return 'yes' unless val 374 | case val 375 | when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' 376 | when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' 377 | else 378 | setup_rb_error "config: --#{@name} accepts only yes/no for argument" 379 | end 380 | end 381 | end 382 | 383 | class PathItem < Item 384 | def config_type 385 | 'path' 386 | end 387 | 388 | private 389 | 390 | def check(path) 391 | setup_rb_error "config: --#{@name} requires argument" unless path 392 | path[0,1] == '$' ? path : File.expand_path(path) 393 | end 394 | end 395 | 396 | class ProgramItem < Item 397 | def config_type 398 | 'program' 399 | end 400 | end 401 | 402 | class SelectItem < Item 403 | def initialize(name, selection, default, desc) 404 | super 405 | @ok = selection.split('/') 406 | end 407 | 408 | def config_type 409 | 'select' 410 | end 411 | 412 | private 413 | 414 | def check(val) 415 | unless @ok.include?(val.strip) 416 | setup_rb_error "config: use --#{@name}=#{@template} (#{val})" 417 | end 418 | val.strip 419 | end 420 | end 421 | 422 | class ExecItem < Item 423 | def initialize(name, selection, desc, &block) 424 | super name, selection, nil, desc 425 | @ok = selection.split('/') 426 | @action = block 427 | end 428 | 429 | def config_type 430 | 'exec' 431 | end 432 | 433 | def value? 434 | false 435 | end 436 | 437 | def resolve(table) 438 | setup_rb_error "$#{name()} wrongly used as option value" 439 | end 440 | 441 | undef set 442 | 443 | def evaluate(val, table) 444 | v = val.strip.downcase 445 | unless @ok.include?(v) 446 | setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" 447 | end 448 | @action.call v, table 449 | end 450 | end 451 | 452 | class PackageSelectionItem < Item 453 | def initialize(name, template, default, help_default, desc) 454 | super name, template, default, desc 455 | @help_default = help_default 456 | end 457 | 458 | attr_reader :help_default 459 | 460 | def config_type 461 | 'package' 462 | end 463 | 464 | private 465 | 466 | def check(val) 467 | unless File.dir?("packages/#{val}") 468 | setup_rb_error "config: no such package: #{val}" 469 | end 470 | val 471 | end 472 | end 473 | 474 | class MetaConfigEnvironment 475 | def initialize(config, installer) 476 | @config = config 477 | @installer = installer 478 | end 479 | 480 | def config_names 481 | @config.names 482 | end 483 | 484 | def config?(name) 485 | @config.key?(name) 486 | end 487 | 488 | def bool_config?(name) 489 | @config.lookup(name).config_type == 'bool' 490 | end 491 | 492 | def path_config?(name) 493 | @config.lookup(name).config_type == 'path' 494 | end 495 | 496 | def value_config?(name) 497 | @config.lookup(name).config_type != 'exec' 498 | end 499 | 500 | def add_config(item) 501 | @config.add item 502 | end 503 | 504 | def add_bool_config(name, default, desc) 505 | @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) 506 | end 507 | 508 | def add_path_config(name, default, desc) 509 | @config.add PathItem.new(name, 'path', default, desc) 510 | end 511 | 512 | def set_config_default(name, default) 513 | @config.lookup(name).default = default 514 | end 515 | 516 | def remove_config(name) 517 | @config.remove(name) 518 | end 519 | 520 | # For only multipackage 521 | def packages 522 | raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer 523 | @installer.packages 524 | end 525 | 526 | # For only multipackage 527 | def declare_packages(list) 528 | raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer 529 | @installer.packages = list 530 | end 531 | end 532 | 533 | end # class ConfigTable 534 | 535 | 536 | # This module requires: #verbose?, #no_harm? 537 | module FileOperations 538 | 539 | def mkdir_p(dirname, prefix = nil) 540 | dirname = prefix + File.expand_path(dirname) if prefix 541 | $stderr.puts "mkdir -p #{dirname}" if verbose? 542 | return if no_harm? 543 | 544 | # Does not check '/', it's too abnormal. 545 | dirs = File.expand_path(dirname).split(%r<(?=/)>) 546 | if /\A[a-z]:\z/i =~ dirs[0] 547 | disk = dirs.shift 548 | dirs[0] = disk + dirs[0] 549 | end 550 | dirs.each_index do |idx| 551 | path = dirs[0..idx].join('') 552 | Dir.mkdir path unless File.dir?(path) 553 | end 554 | end 555 | 556 | def rm_f(path) 557 | $stderr.puts "rm -f #{path}" if verbose? 558 | return if no_harm? 559 | force_remove_file path 560 | end 561 | 562 | def rm_rf(path) 563 | $stderr.puts "rm -rf #{path}" if verbose? 564 | return if no_harm? 565 | remove_tree path 566 | end 567 | 568 | def remove_tree(path) 569 | if File.symlink?(path) 570 | remove_file path 571 | elsif File.dir?(path) 572 | remove_tree0 path 573 | else 574 | force_remove_file path 575 | end 576 | end 577 | 578 | def remove_tree0(path) 579 | Dir.foreach(path) do |ent| 580 | next if ent == '.' 581 | next if ent == '..' 582 | entpath = "#{path}/#{ent}" 583 | if File.symlink?(entpath) 584 | remove_file entpath 585 | elsif File.dir?(entpath) 586 | remove_tree0 entpath 587 | else 588 | force_remove_file entpath 589 | end 590 | end 591 | begin 592 | Dir.rmdir path 593 | rescue Errno::ENOTEMPTY 594 | # directory may not be empty 595 | end 596 | end 597 | 598 | def move_file(src, dest) 599 | force_remove_file dest 600 | begin 601 | File.rename src, dest 602 | rescue 603 | File.open(dest, 'wb') {|f| 604 | f.write File.binread(src) 605 | } 606 | File.chmod File.stat(src).mode, dest 607 | File.unlink src 608 | end 609 | end 610 | 611 | def force_remove_file(path) 612 | begin 613 | remove_file path 614 | rescue 615 | end 616 | end 617 | 618 | def remove_file(path) 619 | File.chmod 0777, path 620 | File.unlink path 621 | end 622 | 623 | def install(from, dest, mode, prefix = nil) 624 | $stderr.puts "install #{from} #{dest}" if verbose? 625 | return if no_harm? 626 | 627 | realdest = prefix ? prefix + File.expand_path(dest) : dest 628 | realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) 629 | str = File.binread(from) 630 | if diff?(str, realdest) 631 | verbose_off { 632 | rm_f realdest if File.exist?(realdest) 633 | } 634 | File.open(realdest, 'wb') {|f| 635 | f.write str 636 | } 637 | File.chmod mode, realdest 638 | 639 | File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| 640 | if prefix 641 | f.puts realdest.sub(prefix, '') 642 | else 643 | f.puts realdest 644 | end 645 | } 646 | end 647 | end 648 | 649 | def diff?(new_content, path) 650 | return true unless File.exist?(path) 651 | new_content != File.binread(path) 652 | end 653 | 654 | def command(*args) 655 | $stderr.puts args.join(' ') if verbose? 656 | system(*args) or raise RuntimeError, 657 | "system(#{args.map{|a| a.inspect }.join(' ')}) failed" 658 | end 659 | 660 | def ruby(*args) 661 | command config('rubyprog'), *args 662 | end 663 | 664 | def make(task = nil) 665 | command(*[config('makeprog'), task].compact) 666 | end 667 | 668 | def extdir?(dir) 669 | File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") 670 | end 671 | 672 | def files_of(dir) 673 | Dir.open(dir) {|d| 674 | return d.select {|ent| File.file?("#{dir}/#{ent}") } 675 | } 676 | end 677 | 678 | DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) 679 | 680 | def directories_of(dir) 681 | Dir.open(dir) {|d| 682 | return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT 683 | } 684 | end 685 | 686 | end 687 | 688 | 689 | # This module requires: #srcdir_root, #objdir_root, #relpath 690 | module HookScriptAPI 691 | 692 | def get_config(key) 693 | @config[key] 694 | end 695 | 696 | alias config get_config 697 | 698 | # obsolete: use metaconfig to change configuration 699 | def set_config(key, val) 700 | @config[key] = val 701 | end 702 | 703 | # 704 | # srcdir/objdir (works only in the package directory) 705 | # 706 | 707 | def curr_srcdir 708 | "#{srcdir_root()}/#{relpath()}" 709 | end 710 | 711 | def curr_objdir 712 | "#{objdir_root()}/#{relpath()}" 713 | end 714 | 715 | def srcfile(path) 716 | "#{curr_srcdir()}/#{path}" 717 | end 718 | 719 | def srcexist?(path) 720 | File.exist?(srcfile(path)) 721 | end 722 | 723 | def srcdirectory?(path) 724 | File.dir?(srcfile(path)) 725 | end 726 | 727 | def srcfile?(path) 728 | File.file?(srcfile(path)) 729 | end 730 | 731 | def srcentries(path = '.') 732 | Dir.open("#{curr_srcdir()}/#{path}") {|d| 733 | return d.to_a - %w(. ..) 734 | } 735 | end 736 | 737 | def srcfiles(path = '.') 738 | srcentries(path).select {|fname| 739 | File.file?(File.join(curr_srcdir(), path, fname)) 740 | } 741 | end 742 | 743 | def srcdirectories(path = '.') 744 | srcentries(path).select {|fname| 745 | File.dir?(File.join(curr_srcdir(), path, fname)) 746 | } 747 | end 748 | 749 | end 750 | 751 | 752 | class ToplevelInstaller 753 | 754 | Version = '3.4.1' 755 | Copyright = 'Copyright (c) 2000-2005 Minero Aoki' 756 | 757 | TASKS = [ 758 | [ 'all', 'do config, setup, then install' ], 759 | [ 'config', 'saves your configurations' ], 760 | [ 'show', 'shows current configuration' ], 761 | [ 'setup', 'compiles ruby extentions and others' ], 762 | [ 'install', 'installs files' ], 763 | [ 'test', 'run all tests in test/' ], 764 | [ 'clean', "does `make clean' for each extention" ], 765 | [ 'distclean',"does `make distclean' for each extention" ] 766 | ] 767 | 768 | def ToplevelInstaller.invoke 769 | config = ConfigTable.new(load_rbconfig()) 770 | config.load_standard_entries 771 | config.load_multipackage_entries if multipackage? 772 | config.fixup 773 | klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) 774 | klass.new(File.dirname($0), config).invoke 775 | end 776 | 777 | def ToplevelInstaller.multipackage? 778 | File.dir?(File.dirname($0) + '/packages') 779 | end 780 | 781 | def ToplevelInstaller.load_rbconfig 782 | if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } 783 | ARGV.delete(arg) 784 | load File.expand_path(arg.split(/=/, 2)[1]) 785 | $".push 'rbconfig.rb' 786 | else 787 | require 'rbconfig' 788 | end 789 | ::Config::CONFIG 790 | end 791 | 792 | def initialize(ardir_root, config) 793 | @ardir = File.expand_path(ardir_root) 794 | @config = config 795 | # cache 796 | @valid_task_re = nil 797 | end 798 | 799 | def config(key) 800 | @config[key] 801 | end 802 | 803 | def inspect 804 | "#<#{self.class} #{__id__()}>" 805 | end 806 | 807 | def invoke 808 | run_metaconfigs 809 | case task = parsearg_global() 810 | when nil, 'all' 811 | parsearg_config 812 | init_installers 813 | exec_config 814 | exec_setup 815 | exec_install 816 | else 817 | case task 818 | when 'config', 'test' 819 | ; 820 | when 'clean', 'distclean' 821 | @config.load_savefile if File.exist?(@config.savefile) 822 | else 823 | @config.load_savefile 824 | end 825 | __send__ "parsearg_#{task}" 826 | init_installers 827 | __send__ "exec_#{task}" 828 | end 829 | end 830 | 831 | def run_metaconfigs 832 | @config.load_script "#{@ardir}/metaconfig" 833 | end 834 | 835 | def init_installers 836 | @installer = Installer.new(@config, @ardir, File.expand_path('.')) 837 | end 838 | 839 | # 840 | # Hook Script API bases 841 | # 842 | 843 | def srcdir_root 844 | @ardir 845 | end 846 | 847 | def objdir_root 848 | '.' 849 | end 850 | 851 | def relpath 852 | '.' 853 | end 854 | 855 | # 856 | # Option Parsing 857 | # 858 | 859 | def parsearg_global 860 | while arg = ARGV.shift 861 | case arg 862 | when /\A\w+\z/ 863 | setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) 864 | return arg 865 | when '-q', '--quiet' 866 | @config.verbose = false 867 | when '--verbose' 868 | @config.verbose = true 869 | when '--help' 870 | print_usage $stdout 871 | exit 0 872 | when '--version' 873 | puts "#{File.basename($0)} version #{Version}" 874 | exit 0 875 | when '--copyright' 876 | puts Copyright 877 | exit 0 878 | else 879 | setup_rb_error "unknown global option '#{arg}'" 880 | end 881 | end 882 | nil 883 | end 884 | 885 | def valid_task?(t) 886 | valid_task_re() =~ t 887 | end 888 | 889 | def valid_task_re 890 | @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ 891 | end 892 | 893 | def parsearg_no_options 894 | unless ARGV.empty? 895 | task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) 896 | setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" 897 | end 898 | end 899 | 900 | alias parsearg_show parsearg_no_options 901 | alias parsearg_setup parsearg_no_options 902 | alias parsearg_test parsearg_no_options 903 | alias parsearg_clean parsearg_no_options 904 | alias parsearg_distclean parsearg_no_options 905 | 906 | def parsearg_config 907 | evalopt = [] 908 | set = [] 909 | @config.config_opt = [] 910 | while i = ARGV.shift 911 | if /\A--?\z/ =~ i 912 | @config.config_opt = ARGV.dup 913 | break 914 | end 915 | name, value = *@config.parse_opt(i) 916 | if @config.value_config?(name) 917 | @config[name] = value 918 | else 919 | evalopt.push [name, value] 920 | end 921 | set.push name 922 | end 923 | evalopt.each do |name, value| 924 | @config.lookup(name).evaluate value, @config 925 | end 926 | # Check if configuration is valid 927 | set.each do |n| 928 | @config[n] if @config.value_config?(n) 929 | end 930 | end 931 | 932 | def parsearg_install 933 | @config.no_harm = false 934 | @config.install_prefix = '' 935 | while a = ARGV.shift 936 | case a 937 | when '--no-harm' 938 | @config.no_harm = true 939 | when /\A--prefix=/ 940 | path = a.split(/=/, 2)[1] 941 | path = File.expand_path(path) unless path[0,1] == '/' 942 | @config.install_prefix = path 943 | else 944 | setup_rb_error "install: unknown option #{a}" 945 | end 946 | end 947 | end 948 | 949 | def print_usage(out) 950 | out.puts 'Typical Installation Procedure:' 951 | out.puts " $ ruby #{File.basename $0} config" 952 | out.puts " $ ruby #{File.basename $0} setup" 953 | out.puts " # ruby #{File.basename $0} install (may require root privilege)" 954 | out.puts 955 | out.puts 'Detailed Usage:' 956 | out.puts " ruby #{File.basename $0} " 957 | out.puts " ruby #{File.basename $0} [] []" 958 | 959 | fmt = " %-24s %s\n" 960 | out.puts 961 | out.puts 'Global options:' 962 | out.printf fmt, '-q,--quiet', 'suppress message outputs' 963 | out.printf fmt, ' --verbose', 'output messages verbosely' 964 | out.printf fmt, ' --help', 'print this message' 965 | out.printf fmt, ' --version', 'print version and quit' 966 | out.printf fmt, ' --copyright', 'print copyright and quit' 967 | out.puts 968 | out.puts 'Tasks:' 969 | TASKS.each do |name, desc| 970 | out.printf fmt, name, desc 971 | end 972 | 973 | fmt = " %-24s %s [%s]\n" 974 | out.puts 975 | out.puts 'Options for CONFIG or ALL:' 976 | @config.each do |item| 977 | out.printf fmt, item.help_opt, item.description, item.help_default 978 | end 979 | out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" 980 | out.puts 981 | out.puts 'Options for INSTALL:' 982 | out.printf fmt, '--no-harm', 'only display what to do if given', 'off' 983 | out.printf fmt, '--prefix=path', 'install path prefix', '' 984 | out.puts 985 | end 986 | 987 | # 988 | # Task Handlers 989 | # 990 | 991 | def exec_config 992 | @installer.exec_config 993 | @config.save # must be final 994 | end 995 | 996 | def exec_setup 997 | @installer.exec_setup 998 | end 999 | 1000 | def exec_install 1001 | @installer.exec_install 1002 | end 1003 | 1004 | def exec_test 1005 | @installer.exec_test 1006 | end 1007 | 1008 | def exec_show 1009 | @config.each do |i| 1010 | printf "%-20s %s\n", i.name, i.value if i.value? 1011 | end 1012 | end 1013 | 1014 | def exec_clean 1015 | @installer.exec_clean 1016 | end 1017 | 1018 | def exec_distclean 1019 | @installer.exec_distclean 1020 | end 1021 | 1022 | end # class ToplevelInstaller 1023 | 1024 | 1025 | class ToplevelInstallerMulti < ToplevelInstaller 1026 | 1027 | include FileOperations 1028 | 1029 | def initialize(ardir_root, config) 1030 | super 1031 | @packages = directories_of("#{@ardir}/packages") 1032 | raise 'no package exists' if @packages.empty? 1033 | @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) 1034 | end 1035 | 1036 | def run_metaconfigs 1037 | @config.load_script "#{@ardir}/metaconfig", self 1038 | @packages.each do |name| 1039 | @config.load_script "#{@ardir}/packages/#{name}/metaconfig" 1040 | end 1041 | end 1042 | 1043 | attr_reader :packages 1044 | 1045 | def packages=(list) 1046 | raise 'package list is empty' if list.empty? 1047 | list.each do |name| 1048 | raise "directory packages/#{name} does not exist"\ 1049 | unless File.dir?("#{@ardir}/packages/#{name}") 1050 | end 1051 | @packages = list 1052 | end 1053 | 1054 | def init_installers 1055 | @installers = {} 1056 | @packages.each do |pack| 1057 | @installers[pack] = Installer.new(@config, 1058 | "#{@ardir}/packages/#{pack}", 1059 | "packages/#{pack}") 1060 | end 1061 | with = extract_selection(config('with')) 1062 | without = extract_selection(config('without')) 1063 | @selected = @installers.keys.select {|name| 1064 | (with.empty? or with.include?(name)) \ 1065 | and not without.include?(name) 1066 | } 1067 | end 1068 | 1069 | def extract_selection(list) 1070 | a = list.split(/,/) 1071 | a.each do |name| 1072 | setup_rb_error "no such package: #{name}" unless @installers.key?(name) 1073 | end 1074 | a 1075 | end 1076 | 1077 | def print_usage(f) 1078 | super 1079 | f.puts 'Inluded packages:' 1080 | f.puts ' ' + @packages.sort.join(' ') 1081 | f.puts 1082 | end 1083 | 1084 | # 1085 | # Task Handlers 1086 | # 1087 | 1088 | def exec_config 1089 | run_hook 'pre-config' 1090 | each_selected_installers {|inst| inst.exec_config } 1091 | run_hook 'post-config' 1092 | @config.save # must be final 1093 | end 1094 | 1095 | def exec_setup 1096 | run_hook 'pre-setup' 1097 | each_selected_installers {|inst| inst.exec_setup } 1098 | run_hook 'post-setup' 1099 | end 1100 | 1101 | def exec_install 1102 | run_hook 'pre-install' 1103 | each_selected_installers {|inst| inst.exec_install } 1104 | run_hook 'post-install' 1105 | end 1106 | 1107 | def exec_test 1108 | run_hook 'pre-test' 1109 | each_selected_installers {|inst| inst.exec_test } 1110 | run_hook 'post-test' 1111 | end 1112 | 1113 | def exec_clean 1114 | rm_f @config.savefile 1115 | run_hook 'pre-clean' 1116 | each_selected_installers {|inst| inst.exec_clean } 1117 | run_hook 'post-clean' 1118 | end 1119 | 1120 | def exec_distclean 1121 | rm_f @config.savefile 1122 | run_hook 'pre-distclean' 1123 | each_selected_installers {|inst| inst.exec_distclean } 1124 | run_hook 'post-distclean' 1125 | end 1126 | 1127 | # 1128 | # lib 1129 | # 1130 | 1131 | def each_selected_installers 1132 | Dir.mkdir 'packages' unless File.dir?('packages') 1133 | @selected.each do |pack| 1134 | $stderr.puts "Processing the package `#{pack}' ..." if verbose? 1135 | Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") 1136 | Dir.chdir "packages/#{pack}" 1137 | yield @installers[pack] 1138 | Dir.chdir '../..' 1139 | end 1140 | end 1141 | 1142 | def run_hook(id) 1143 | @root_installer.run_hook id 1144 | end 1145 | 1146 | # module FileOperations requires this 1147 | def verbose? 1148 | @config.verbose? 1149 | end 1150 | 1151 | # module FileOperations requires this 1152 | def no_harm? 1153 | @config.no_harm? 1154 | end 1155 | 1156 | end # class ToplevelInstallerMulti 1157 | 1158 | 1159 | class Installer 1160 | 1161 | FILETYPES = %w( bin lib ext data conf man ) 1162 | 1163 | include FileOperations 1164 | include HookScriptAPI 1165 | 1166 | def initialize(config, srcroot, objroot) 1167 | @config = config 1168 | @srcdir = File.expand_path(srcroot) 1169 | @objdir = File.expand_path(objroot) 1170 | @currdir = '.' 1171 | end 1172 | 1173 | def inspect 1174 | "#<#{self.class} #{File.basename(@srcdir)}>" 1175 | end 1176 | 1177 | def noop(rel) 1178 | end 1179 | 1180 | # 1181 | # Hook Script API base methods 1182 | # 1183 | 1184 | def srcdir_root 1185 | @srcdir 1186 | end 1187 | 1188 | def objdir_root 1189 | @objdir 1190 | end 1191 | 1192 | def relpath 1193 | @currdir 1194 | end 1195 | 1196 | # 1197 | # Config Access 1198 | # 1199 | 1200 | # module FileOperations requires this 1201 | def verbose? 1202 | @config.verbose? 1203 | end 1204 | 1205 | # module FileOperations requires this 1206 | def no_harm? 1207 | @config.no_harm? 1208 | end 1209 | 1210 | def verbose_off 1211 | begin 1212 | save, @config.verbose = @config.verbose?, false 1213 | yield 1214 | ensure 1215 | @config.verbose = save 1216 | end 1217 | end 1218 | 1219 | # 1220 | # TASK config 1221 | # 1222 | 1223 | def exec_config 1224 | exec_task_traverse 'config' 1225 | end 1226 | 1227 | alias config_dir_bin noop 1228 | alias config_dir_lib noop 1229 | 1230 | def config_dir_ext(rel) 1231 | extconf if extdir?(curr_srcdir()) 1232 | end 1233 | 1234 | alias config_dir_data noop 1235 | alias config_dir_conf noop 1236 | alias config_dir_man noop 1237 | 1238 | def extconf 1239 | ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt 1240 | end 1241 | 1242 | # 1243 | # TASK setup 1244 | # 1245 | 1246 | def exec_setup 1247 | exec_task_traverse 'setup' 1248 | end 1249 | 1250 | def setup_dir_bin(rel) 1251 | files_of(curr_srcdir()).each do |fname| 1252 | update_shebang_line "#{curr_srcdir()}/#{fname}" 1253 | end 1254 | end 1255 | 1256 | alias setup_dir_lib noop 1257 | 1258 | def setup_dir_ext(rel) 1259 | make if extdir?(curr_srcdir()) 1260 | end 1261 | 1262 | alias setup_dir_data noop 1263 | alias setup_dir_conf noop 1264 | alias setup_dir_man noop 1265 | 1266 | def update_shebang_line(path) 1267 | return if no_harm? 1268 | return if config('shebang') == 'never' 1269 | old = Shebang.load(path) 1270 | if old 1271 | $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 1272 | new = new_shebang(old) 1273 | return if new.to_s == old.to_s 1274 | else 1275 | return unless config('shebang') == 'all' 1276 | new = Shebang.new(config('rubypath')) 1277 | end 1278 | $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? 1279 | open_atomic_writer(path) {|output| 1280 | File.open(path, 'rb') {|f| 1281 | f.gets if old # discard 1282 | output.puts new.to_s 1283 | output.print f.read 1284 | } 1285 | } 1286 | end 1287 | 1288 | def new_shebang(old) 1289 | if /\Aruby/ =~ File.basename(old.cmd) 1290 | Shebang.new(config('rubypath'), old.args) 1291 | elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' 1292 | Shebang.new(config('rubypath'), old.args[1..-1]) 1293 | else 1294 | return old unless config('shebang') == 'all' 1295 | Shebang.new(config('rubypath')) 1296 | end 1297 | end 1298 | 1299 | def open_atomic_writer(path, &block) 1300 | tmpfile = File.basename(path) + '.tmp' 1301 | begin 1302 | File.open(tmpfile, 'wb', &block) 1303 | File.rename tmpfile, File.basename(path) 1304 | ensure 1305 | File.unlink tmpfile if File.exist?(tmpfile) 1306 | end 1307 | end 1308 | 1309 | class Shebang 1310 | def Shebang.load(path) 1311 | line = nil 1312 | File.open(path) {|f| 1313 | line = f.gets 1314 | } 1315 | return nil unless /\A#!/ =~ line 1316 | parse(line) 1317 | end 1318 | 1319 | def Shebang.parse(line) 1320 | cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') 1321 | new(cmd, args) 1322 | end 1323 | 1324 | def initialize(cmd, args = []) 1325 | @cmd = cmd 1326 | @args = args 1327 | end 1328 | 1329 | attr_reader :cmd 1330 | attr_reader :args 1331 | 1332 | def to_s 1333 | "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") 1334 | end 1335 | end 1336 | 1337 | # 1338 | # TASK install 1339 | # 1340 | 1341 | def exec_install 1342 | rm_f 'InstalledFiles' 1343 | exec_task_traverse 'install' 1344 | end 1345 | 1346 | def install_dir_bin(rel) 1347 | install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 1348 | end 1349 | 1350 | def install_dir_lib(rel) 1351 | install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 1352 | end 1353 | 1354 | def install_dir_ext(rel) 1355 | return unless extdir?(curr_srcdir()) 1356 | install_files rubyextentions('.'), 1357 | "#{config('sodir')}/#{File.dirname(rel)}", 1358 | 0555 1359 | end 1360 | 1361 | def install_dir_data(rel) 1362 | install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 1363 | end 1364 | 1365 | def install_dir_conf(rel) 1366 | # FIXME: should not remove current config files 1367 | # (rename previous file to .old/.org) 1368 | install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 1369 | end 1370 | 1371 | def install_dir_man(rel) 1372 | install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 1373 | end 1374 | 1375 | def install_files(list, dest, mode) 1376 | mkdir_p dest, @config.install_prefix 1377 | list.each do |fname| 1378 | install fname, dest, mode, @config.install_prefix 1379 | end 1380 | end 1381 | 1382 | def libfiles 1383 | glob_reject(%w(*.y *.output), targetfiles()) 1384 | end 1385 | 1386 | def rubyextentions(dir) 1387 | ents = glob_select("*.#{@config.dllext}", targetfiles()) 1388 | if ents.empty? 1389 | setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" 1390 | end 1391 | ents 1392 | end 1393 | 1394 | def targetfiles 1395 | mapdir(existfiles() - hookfiles()) 1396 | end 1397 | 1398 | def mapdir(ents) 1399 | ents.map {|ent| 1400 | if File.exist?(ent) 1401 | then ent # objdir 1402 | else "#{curr_srcdir()}/#{ent}" # srcdir 1403 | end 1404 | } 1405 | end 1406 | 1407 | # picked up many entries from cvs-1.11.1/src/ignore.c 1408 | JUNK_FILES = %w( 1409 | core RCSLOG tags TAGS .make.state 1410 | .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb 1411 | *~ *.old *.bak *.BAK *.orig *.rej _$* *$ 1412 | 1413 | *.org *.in .* 1414 | ) 1415 | 1416 | def existfiles 1417 | glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) 1418 | end 1419 | 1420 | def hookfiles 1421 | %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| 1422 | %w( config setup install clean ).map {|t| sprintf(fmt, t) } 1423 | }.flatten 1424 | end 1425 | 1426 | def glob_select(pat, ents) 1427 | re = globs2re([pat]) 1428 | ents.select {|ent| re =~ ent } 1429 | end 1430 | 1431 | def glob_reject(pats, ents) 1432 | re = globs2re(pats) 1433 | ents.reject {|ent| re =~ ent } 1434 | end 1435 | 1436 | GLOB2REGEX = { 1437 | '.' => '\.', 1438 | '$' => '\$', 1439 | '#' => '\#', 1440 | '*' => '.*' 1441 | } 1442 | 1443 | def globs2re(pats) 1444 | /\A(?:#{ 1445 | pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') 1446 | })\z/ 1447 | end 1448 | 1449 | # 1450 | # TASK test 1451 | # 1452 | 1453 | TESTDIR = 'test' 1454 | 1455 | def exec_test 1456 | unless File.directory?('test') 1457 | $stderr.puts 'no test in this package' if verbose? 1458 | return 1459 | end 1460 | $stderr.puts 'Running tests...' if verbose? 1461 | begin 1462 | require 'test/unit' 1463 | rescue LoadError 1464 | setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' 1465 | end 1466 | runner = Test::Unit::AutoRunner.new(true) 1467 | runner.to_run << TESTDIR 1468 | runner.run 1469 | end 1470 | 1471 | # 1472 | # TASK clean 1473 | # 1474 | 1475 | def exec_clean 1476 | exec_task_traverse 'clean' 1477 | rm_f @config.savefile 1478 | rm_f 'InstalledFiles' 1479 | end 1480 | 1481 | alias clean_dir_bin noop 1482 | alias clean_dir_lib noop 1483 | alias clean_dir_data noop 1484 | alias clean_dir_conf noop 1485 | alias clean_dir_man noop 1486 | 1487 | def clean_dir_ext(rel) 1488 | return unless extdir?(curr_srcdir()) 1489 | make 'clean' if File.file?('Makefile') 1490 | end 1491 | 1492 | # 1493 | # TASK distclean 1494 | # 1495 | 1496 | def exec_distclean 1497 | exec_task_traverse 'distclean' 1498 | rm_f @config.savefile 1499 | rm_f 'InstalledFiles' 1500 | end 1501 | 1502 | alias distclean_dir_bin noop 1503 | alias distclean_dir_lib noop 1504 | 1505 | def distclean_dir_ext(rel) 1506 | return unless extdir?(curr_srcdir()) 1507 | make 'distclean' if File.file?('Makefile') 1508 | end 1509 | 1510 | alias distclean_dir_data noop 1511 | alias distclean_dir_conf noop 1512 | alias distclean_dir_man noop 1513 | 1514 | # 1515 | # Traversing 1516 | # 1517 | 1518 | def exec_task_traverse(task) 1519 | run_hook "pre-#{task}" 1520 | FILETYPES.each do |type| 1521 | if type == 'ext' and config('without-ext') == 'yes' 1522 | $stderr.puts 'skipping ext/* by user option' if verbose? 1523 | next 1524 | end 1525 | traverse task, type, "#{task}_dir_#{type}" 1526 | end 1527 | run_hook "post-#{task}" 1528 | end 1529 | 1530 | def traverse(task, rel, mid) 1531 | dive_into(rel) { 1532 | run_hook "pre-#{task}" 1533 | __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') 1534 | directories_of(curr_srcdir()).each do |d| 1535 | traverse task, "#{rel}/#{d}", mid 1536 | end 1537 | run_hook "post-#{task}" 1538 | } 1539 | end 1540 | 1541 | def dive_into(rel) 1542 | return unless File.dir?("#{@srcdir}/#{rel}") 1543 | 1544 | dir = File.basename(rel) 1545 | Dir.mkdir dir unless File.dir?(dir) 1546 | prevdir = Dir.pwd 1547 | Dir.chdir dir 1548 | $stderr.puts '---> ' + rel if verbose? 1549 | @currdir = rel 1550 | yield 1551 | Dir.chdir prevdir 1552 | $stderr.puts '<--- ' + rel if verbose? 1553 | @currdir = File.dirname(rel) 1554 | end 1555 | 1556 | def run_hook(id) 1557 | path = [ "#{curr_srcdir()}/#{id}", 1558 | "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } 1559 | return unless path 1560 | begin 1561 | instance_eval File.read(path), path, 1 1562 | rescue 1563 | raise if $DEBUG 1564 | setup_rb_error "hook #{path} failed:\n" + $!.message 1565 | end 1566 | end 1567 | 1568 | end # class Installer 1569 | 1570 | 1571 | class SetupError < StandardError; end 1572 | 1573 | def setup_rb_error(msg) 1574 | raise SetupError, msg 1575 | end 1576 | 1577 | if $0 == __FILE__ 1578 | begin 1579 | ToplevelInstaller.invoke 1580 | rescue SetupError 1581 | raise if $DEBUG 1582 | $stderr.puts $!.message 1583 | $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." 1584 | exit 1 1585 | end 1586 | end 1587 | --------------------------------------------------------------------------------