extractexifgps 0.1.0.alpha → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 520e90a163beaf80af6f69fa4e0583a9e2729f4289917a1563980e80395eb2e3
4
- data.tar.gz: 0464be9c8c7c14b94ad119b6498e00402f333d8dc3ed7bc49e7c9db39a52cbc9
3
+ metadata.gz: 7142bb7da371c0971216e138a8ca090320a7dc7117426fbc93c237fd9d59d321
4
+ data.tar.gz: 176e49db29381207153297d529f259b5ce4b9e2029eaceefdb2cbaaa21db32c7
5
5
  SHA512:
6
- metadata.gz: f4a69e1094d31aff3fe02e2b6004cb809b82aa1b6f83a75a1357d1e8d4fcb11cba88c5ab2e336777c9d96c16076284a8d19b63057a736b4c13c71b55a93116be
7
- data.tar.gz: 02ff1a5af99c9880936a9feb4f80e1c48e17f3f04b6c9e08c5ebb52810379cfaa2d80bbe9f8dbc09d34e212241770c03483f1d5e29368c247662ee59bd8b9b75
6
+ metadata.gz: 6ec235f10e4ab9287a7c01ac617aef631f405788297727caac3d8f42000b64eed6a9eafa35d1821ad7ecc64bca56e5d196fb88cac12c39ff1b8e5a389a69d008
7
+ data.tar.gz: 7fc72ac70efc953836081c880912a22f84416fd320fda66d84983c8d2f8c976d0d93e60b787308ff4a5e0e7f0cff8e994bb31d2f3c40b5ab582789c58db1ba03
data/.rubocop.yml ADDED
@@ -0,0 +1,35 @@
1
+ AllCops:
2
+ Exclude:
3
+ - test/**/*
4
+ - "*.gemspec"
5
+
6
+ TargetRubyVersion:
7
+ 2.5
8
+
9
+ GuardClause:
10
+ MinBodyLength: 3
11
+
12
+ Layout/DotPosition:
13
+ EnforcedStyle: trailing
14
+
15
+ MethodLength:
16
+ Severity: warning
17
+ Max: 15
18
+
19
+ Metrics/AbcSize:
20
+ Max: 20
21
+
22
+ Style/AndOr:
23
+ Enabled: false
24
+
25
+ Style/AsciiComments:
26
+ Enabled: false
27
+
28
+ Style/ClassAndModuleChildren:
29
+ Enabled: False
30
+
31
+ Style/StderrPuts:
32
+ Enabled: False
33
+
34
+ Style/FrozenStringLiteralComment:
35
+ Enabled: false
data/Gemfile CHANGED
@@ -1,20 +1,21 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'exif', '~> 2.2'
4
+ gem 'thor', '~> 0.20'
4
5
 
5
6
  group :development do
6
7
  gem 'bundler', '~> 1.11'
7
- gem 'jeweler', '~> 2.3.9'
8
8
  gem 'byebug', '~> 8.2'
9
+ gem 'jeweler', '~> 2.3.9'
9
10
 
10
- gem 'yard', '~> 0.7'
11
11
  gem 'rdoc', '~> 3.12'
12
+ gem 'yard', '~> 0.7'
12
13
 
14
+ gem 'guard', '~> 2.14'
15
+ gem 'guard-minitest', '~> 2.4'
13
16
  gem 'minitest', '~> 5.0'
14
17
  gem 'minitest-reporters', '~> 1.1'
15
18
  gem 'simplecov', '~> 0.11'
16
- gem 'guard', '~> 2.14'
17
- gem 'guard-minitest', '~> 2.4'
18
19
 
19
20
  gem 'rubocop', '~> 0.48'
20
21
  end
data/Gemfile.lock CHANGED
@@ -134,6 +134,7 @@ DEPENDENCIES
134
134
  rdoc (~> 3.12)
135
135
  rubocop (~> 0.48)
136
136
  simplecov (~> 0.11)
137
+ thor (~> 0.20)
137
138
  yard (~> 0.7)
138
139
 
139
140
  BUNDLED WITH
data/README.md CHANGED
@@ -14,9 +14,64 @@ gem install extractexifgps
14
14
  ```sh
15
15
  extractexifgps > images_in_current_directory.csv
16
16
  extractexifgps /some/directory/ > images_in_some_directory.csv
17
+
18
+ # CSV format
19
+ extractexifgps csv ./
20
+ extractexifgps -C ./ # short alias
21
+ extractexifgps csv -r ./ # file images recursively
22
+ extractexifgps csv -o /path/to/output.csv ./ # write directly to a file
23
+
24
+ # HTML format
25
+ extractexifgps html ./
26
+ extractexifgps -H ./ # short alias
27
+ extractexifgps html -r ./ # file images recursively
28
+ extractexifgps html -o /path/to/output.html ./ # write directly to a file
29
+ extractexifgps html -t /path/to/template.erb ./ # use a custom ERB template
30
+ ```
31
+
32
+ ### HTML Templates
33
+
34
+ `extractexifgps` comes with a default template to generate a decent-looking
35
+ HTML table, but you can supply your own
36
+ [ERB](https://ruby-doc.org/stdlib-2.5.1/libdoc/erb/rdoc/ERB.html) template to
37
+ generate any kind of HTML file you like. See the built-in
38
+ [templates/basic.html.erb](templates/basic.html.erb) as an example template.
39
+
40
+
41
+ ### Command-line Help
42
+
43
+ You can get information about the command-line interface in general, or for
44
+ specific commands:
45
+
46
+ ```sh
47
+ extractexifgps help
48
+ extractexifgps help csv
49
+ extractexifgps help html
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ### Testing
55
+
56
+ A few Rake commands will help your testing:
57
+
58
+ - `rake test`: Run the test suite
59
+ - `rake lint`: Run the code linters
60
+ - `rake`: Run all tests and linters
61
+
62
+ To facilitate development, consider running `guard` in the background while you
63
+ work. Whenver a source file it changed, it will automatically run the relevent
64
+ tests. This will provide you immediate test feedback at all times.
65
+
66
+ ### Documentation
67
+
68
+ Generate code docs with:
69
+
70
+ ```sh
71
+ rake doc
17
72
  ```
18
73
 
19
- ## Contributing to ExtractExifGps
74
+ ### Contributing to ExtractExifGps
20
75
 
21
76
  * Check out the latest master to make sure the feature hasn't been
22
77
  implemented or the bug hasn't been fixed yet.
@@ -30,7 +85,3 @@ extractexifgps /some/directory/ > images_in_some_directory.csv
30
85
  * Please try not to mess with the Rakefile, version, or history. If you want
31
86
  to have your own version, or is otherwise necessary, that is fine, but
32
87
  please isolate to its own commit so I can cherry-pick around it.
33
-
34
- ## Copyright
35
-
36
- Copyright (c) 2018 Jon Sangster. See LICENSE.txt for further details.
data/Rakefile CHANGED
@@ -1,12 +1,10 @@
1
- # encoding: utf-8
2
-
3
1
  require 'rubygems'
4
2
  require 'bundler'
5
3
  begin
6
4
  Bundler.setup(:default, :development)
7
5
  rescue Bundler::BundlerError => e
8
6
  $stderr.puts e.message
9
- $stderr.puts "Run `bundle install` to install missing gems"
7
+ $stderr.puts 'Run `bundle install` to install missing gems'
10
8
  exit e.status_code
11
9
  end
12
10
 
@@ -18,12 +16,12 @@ Jeweler::Tasks.new do |gem|
18
16
  gem.name = 'extractexifgps'
19
17
  gem.homepage = 'http://github.com/sangster/extractexifgps'
20
18
  gem.license = 'MIT'
21
- gem.summary = %Q{Extracts EXIF GPS data from images}
22
- gem.description = %Q{Extracts EXIF GPS data from images}
19
+ gem.summary = %(Extracts EXIF GPS data from images)
20
+ gem.description = %(Extracts EXIF GPS data from images)
23
21
  gem.email = 'jon@ertt.ca'
24
22
  gem.authors = ['Jon Sangster']
25
23
  gem.version = ExtractExifGps::Version::STRING
26
- gem.executables = %W{extractexifgps}
24
+ gem.executables = %w[extractexifgps]
27
25
  end
28
26
  Jeweler::RubygemsDotOrgTasks.new
29
27
 
@@ -36,12 +34,10 @@ end
36
34
 
37
35
  desc 'Code coverage detail'
38
36
  task :simplecov do
39
- ENV['COVERAGE'] = "true"
37
+ ENV['COVERAGE'] = 'true'
40
38
  Rake::Task['test'].execute
41
39
  end
42
40
 
43
- task :default => :test
44
-
45
41
  require 'rdoc/task'
46
42
  Rake::RDocTask.new do |rdoc|
47
43
  version = File.exist?('VERSION') ? File.read('VERSION') : ''
@@ -51,3 +47,19 @@ Rake::RDocTask.new do |rdoc|
51
47
  rdoc.rdoc_files.include('README*')
52
48
  rdoc.rdoc_files.include('lib/**/*.rb')
53
49
  end
50
+
51
+ require 'yard'
52
+ YARD::Rake::YardocTask.new do |t|
53
+ t.files = ['lib/**/*.rb']
54
+ end
55
+
56
+ require 'rubocop/rake_task'
57
+ RuboCop::RakeTask.new
58
+
59
+ desc 'Run all linters'
60
+ task lint: [:rubocop]
61
+
62
+ desc 'Run all tests and linters'
63
+ task check: %i[test rubocop]
64
+
65
+ task default: :check
data/bin/extractexifgps CHANGED
@@ -1,6 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
-
3
2
  require 'extractexifgps'
4
-
5
- extractor = ExtractExifGps::DirectoryFilesExtractor.new(ARGV[0] || Dir.pwd)
6
- puts extractor.to_csv
3
+ ExtractExifGps::Cli.start(ARGV)
@@ -0,0 +1,109 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: extractexifgps 1.0.0 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "extractexifgps".freeze
9
+ s.version = "1.0.0"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib".freeze]
13
+ s.authors = ["Jon Sangster".freeze]
14
+ s.date = "2018-08-11"
15
+ s.description = "Extracts EXIF GPS data from images".freeze
16
+ s.email = "jon@ertt.ca".freeze
17
+ s.executables = ["extractexifgps".freeze]
18
+ s.extra_rdoc_files = [
19
+ "LICENSE.txt",
20
+ "README.md"
21
+ ]
22
+ s.files = [
23
+ ".document",
24
+ ".rubocop.yml",
25
+ ".ruby_version",
26
+ "Gemfile",
27
+ "Gemfile.lock",
28
+ "Guardfile",
29
+ "LICENSE.txt",
30
+ "README.md",
31
+ "Rakefile",
32
+ "bin/extractexifgps",
33
+ "extractexifgps.gemspec",
34
+ "lib/cli.rb",
35
+ "lib/coord.rb",
36
+ "lib/csv_renderer.rb",
37
+ "lib/extractexifgps.rb",
38
+ "lib/file_set.rb",
39
+ "lib/gps_extractor.rb",
40
+ "lib/html_renderer.rb",
41
+ "templates/basic.html.erb",
42
+ "test/coord_test.rb",
43
+ "test/csv_renderer_test.rb",
44
+ "test/extractexifgps_test.rb",
45
+ "test/helper.rb",
46
+ "test/html_renderer_test.rb",
47
+ "test/images/cats/.DS_Store",
48
+ "test/images/cats/image_e.jpg",
49
+ "test/images/image_a.jpg",
50
+ "test/images/image_b.jpg",
51
+ "test/images/image_c.jpg",
52
+ "test/images/image_d.jpg",
53
+ "test/integration/file_set_test.rb",
54
+ "test/integration/gps_extractor_test.rb"
55
+ ]
56
+ s.homepage = "http://github.com/sangster/extractexifgps".freeze
57
+ s.licenses = ["MIT".freeze]
58
+ s.rubygems_version = "2.7.6".freeze
59
+ s.summary = "Extracts EXIF GPS data from images".freeze
60
+
61
+ if s.respond_to? :specification_version then
62
+ s.specification_version = 4
63
+
64
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
65
+ s.add_runtime_dependency(%q<exif>.freeze, ["~> 2.2"])
66
+ s.add_runtime_dependency(%q<thor>.freeze, ["~> 0.20"])
67
+ s.add_development_dependency(%q<bundler>.freeze, ["~> 1.11"])
68
+ s.add_development_dependency(%q<byebug>.freeze, ["~> 8.2"])
69
+ s.add_development_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
70
+ s.add_development_dependency(%q<rdoc>.freeze, ["~> 3.12"])
71
+ s.add_development_dependency(%q<yard>.freeze, ["~> 0.7"])
72
+ s.add_development_dependency(%q<guard>.freeze, ["~> 2.14"])
73
+ s.add_development_dependency(%q<guard-minitest>.freeze, ["~> 2.4"])
74
+ s.add_development_dependency(%q<minitest>.freeze, ["~> 5.0"])
75
+ s.add_development_dependency(%q<minitest-reporters>.freeze, ["~> 1.1"])
76
+ s.add_development_dependency(%q<simplecov>.freeze, ["~> 0.11"])
77
+ s.add_development_dependency(%q<rubocop>.freeze, ["~> 0.48"])
78
+ else
79
+ s.add_dependency(%q<exif>.freeze, ["~> 2.2"])
80
+ s.add_dependency(%q<thor>.freeze, ["~> 0.20"])
81
+ s.add_dependency(%q<bundler>.freeze, ["~> 1.11"])
82
+ s.add_dependency(%q<byebug>.freeze, ["~> 8.2"])
83
+ s.add_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
84
+ s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
85
+ s.add_dependency(%q<yard>.freeze, ["~> 0.7"])
86
+ s.add_dependency(%q<guard>.freeze, ["~> 2.14"])
87
+ s.add_dependency(%q<guard-minitest>.freeze, ["~> 2.4"])
88
+ s.add_dependency(%q<minitest>.freeze, ["~> 5.0"])
89
+ s.add_dependency(%q<minitest-reporters>.freeze, ["~> 1.1"])
90
+ s.add_dependency(%q<simplecov>.freeze, ["~> 0.11"])
91
+ s.add_dependency(%q<rubocop>.freeze, ["~> 0.48"])
92
+ end
93
+ else
94
+ s.add_dependency(%q<exif>.freeze, ["~> 2.2"])
95
+ s.add_dependency(%q<thor>.freeze, ["~> 0.20"])
96
+ s.add_dependency(%q<bundler>.freeze, ["~> 1.11"])
97
+ s.add_dependency(%q<byebug>.freeze, ["~> 8.2"])
98
+ s.add_dependency(%q<jeweler>.freeze, ["~> 2.3.9"])
99
+ s.add_dependency(%q<rdoc>.freeze, ["~> 3.12"])
100
+ s.add_dependency(%q<yard>.freeze, ["~> 0.7"])
101
+ s.add_dependency(%q<guard>.freeze, ["~> 2.14"])
102
+ s.add_dependency(%q<guard-minitest>.freeze, ["~> 2.4"])
103
+ s.add_dependency(%q<minitest>.freeze, ["~> 5.0"])
104
+ s.add_dependency(%q<minitest-reporters>.freeze, ["~> 1.1"])
105
+ s.add_dependency(%q<simplecov>.freeze, ["~> 0.11"])
106
+ s.add_dependency(%q<rubocop>.freeze, ["~> 0.48"])
107
+ end
108
+ end
109
+
data/lib/cli.rb ADDED
@@ -0,0 +1,58 @@
1
+ require 'thor'
2
+
3
+ require_relative 'csv_renderer'
4
+ require_relative 'html_renderer'
5
+ require_relative 'file_set'
6
+
7
+ module ExtractExifGps
8
+ # Defines the CLI interface for this application.
9
+ #
10
+ # You can get information about command-line options with
11
+ # +extractexifgps help+ or +extractexifgps help <command>+ to information
12
+ # pertaining to the particular command.
13
+ class Cli < Thor
14
+ default_command :csv
15
+ map '-C' => :csv
16
+ map '-H' => :html
17
+
18
+ class_option :recursive, type: :boolean, aliases: %w[-r],
19
+ desc: 'Recursively search subdirectories'
20
+ class_option :output, type: :string, aliases: %w[-o],
21
+ desc: 'Write to OUTPUT instead of stdout'
22
+
23
+ desc 'csv [OPTION]... [DIR]...',
24
+ 'Export EXIF GPS data from the images in DIR in CSV format'
25
+ def csv(*dirs)
26
+ extractor = ExtractExifGps::GpsExtractor.new(
27
+ FileSet.new(dirs.empty? ? ['.'] : dirs, recursive: options[:recursive])
28
+ )
29
+
30
+ $stdout.reopen(options[:output], 'w') if options[:output]
31
+ puts CsvRenderer.new(extractor)
32
+ end
33
+
34
+ option :template, type: :string, aliases: %w[-t],
35
+ desc: 'Generate the HTML with TEMPLATE'
36
+ desc 'html [OPTION]... [DIR]...',
37
+ 'Export EXIF GPS data from the images in DIR in HTML format'
38
+ def html(*dirs)
39
+ search_paths = dirs.empty? ? ['.'] : dirs
40
+ extractor = ExtractExifGps::GpsExtractor.new(
41
+ FileSet.new(search_paths, recursive: options[:recursive])
42
+ )
43
+
44
+ template = options[:template] || template_path('templates/basic.html.erb')
45
+
46
+ $stdout.reopen(options[:output], 'w') if options[:output]
47
+ puts HtmlRenderer.new(extractor,
48
+ search_paths: search_paths,
49
+ template: template)
50
+ end
51
+
52
+ private
53
+
54
+ def template_path(path)
55
+ Pathname.new(__FILE__).join('../../').join(path)
56
+ end
57
+ end
58
+ end
@@ -1,15 +1,17 @@
1
1
  module ExtractExifGps
2
- class Coords
3
- GPS_ICONS = %w{° ' "}.freeze
2
+ # Represents a single coordinate, either latitude or longitude, in a GPS
3
+ # coordinate.
4
+ class Coord
5
+ GPS_ICONS = %w[° ' "].freeze
4
6
  MAX_DECIMALS = 5
5
7
 
6
8
  class << self
7
9
  # @param [Exif::Data] data
8
- # @return [Hash<Symbol, Coords>, nil] the pair of GPS coordinates from in
10
+ # @return [Hash<Symbol, Coord>, nil] the pair of GPS coordinates from in
9
11
  # the given EXIF data, or +nil+ if there is none
10
12
  def from_exif(data)
11
13
  if (gps = data&.[](:gps))&.any?
12
- {lat: extract(gps, :latitude), lon: extract(gps, :longitude)}
14
+ { lat: extract(gps, :latitude), lon: extract(gps, :longitude) }
13
15
  end
14
16
  end
15
17
 
@@ -20,11 +22,17 @@ module ExtractExifGps
20
22
  end
21
23
  end
22
24
 
25
+ # @param dir [#to_s] A charcter representing a cardinal direction: N, E,
26
+ # S, W
27
+ # @param coords [Array<Number>] An array containing the coordinate 1-3
28
+ # numbers, representing degrees, minutes, and seconds
23
29
  def initialize(dir, coords)
24
30
  @dir = dir
25
31
  @coords = coords
26
32
  end
27
33
 
34
+ # @return [String] formats the given coordinate in degrees, minutes, and
35
+ # secondes. ex: +N 38° 24' 10"+ +W 122° 49' 20"+
28
36
  def to_s
29
37
  parts = @coords.reject(&:zero?).map(&method(:rational_str)).zip(GPS_ICONS)
30
38
  "#{@dir} #{parts.map(&:join).join(' ')}"
@@ -0,0 +1,23 @@
1
+ require 'csv'
2
+
3
+ module ExtractExifGps
4
+ # Render a {FileSet} as a CSV file
5
+ class CsvRenderer
6
+ CSV_COLUMNS = %w[Path Latitude Longitude].freeze
7
+
8
+ # @param files [FileSet]
9
+ def initialize(files)
10
+ @files = files
11
+ end
12
+
13
+ def to_s
14
+ CSV.generate do |csv|
15
+ csv << CSV_COLUMNS
16
+
17
+ @files.each do |item|
18
+ csv << [item[:path], item[:lat]&.to_s, item[:lon]&.to_s]
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,12 +1,13 @@
1
1
  module ExtractExifGps
2
2
  module Version
3
- MAJOR = 0
4
- MINOR = 1
3
+ MAJOR = 1
4
+ MINOR = 0
5
5
  PATCH = 0
6
- BUILD = 'alpha'
6
+ BUILD = nil
7
7
 
8
8
  STRING = [MAJOR, MINOR, PATCH, BUILD].compact.join('.')
9
9
  end
10
10
  end
11
11
 
12
- require_relative 'directory_files_extractor'
12
+ require_relative 'cli'
13
+ require_relative 'gps_extractor'
data/lib/file_set.rb ADDED
@@ -0,0 +1,28 @@
1
+ require 'pathname'
2
+
3
+ module ExtractExifGps
4
+ # Enumerates over all the files within one or more given directories,
5
+ # optionally including files in subdirectories.
6
+ class FileSet
7
+ include Enumerable
8
+
9
+ # @param dirs [Array,String] one more paths to search
10
+ # @param recursive [Boolean] if this file set should include files from
11
+ # subdirectories
12
+ def initialize(dirs, recursive: false)
13
+ @dirs = Array(dirs)
14
+ @recursive = recursive
15
+ end
16
+
17
+ def each(&block)
18
+ @dirs.flat_map { |d| Pathname.new(d).glob(glob_pattern).select(&:file?) }.
19
+ each(&block)
20
+ end
21
+
22
+ private
23
+
24
+ def glob_pattern
25
+ @recursive ? '**/*' : '*'
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ require 'csv'
2
+ require 'exif'
3
+
4
+ require_relative 'coord'
5
+
6
+ module ExtractExifGps
7
+ # Reads EXIF data from a given {FileSet} and extracts any available GPS data
8
+ class GpsExtractor
9
+ extend Forwardable
10
+ include Enumerable
11
+
12
+ def_delegator :gps_list, :each
13
+
14
+ # @param fileset [FileSet]
15
+ def initialize(file_set)
16
+ @file_set = file_set
17
+ end
18
+
19
+ private
20
+
21
+ def gps_list
22
+ exif_list.map do |item|
23
+ gps = Coord.from_exif(item[:exif])
24
+ { path: item[:path], lat: gps&.[](:lat), lon: gps&.[](:lon) }
25
+ end
26
+ end
27
+
28
+ def exif_list
29
+ @file_set.map { |file| { path: file, exif: exif(file) } }
30
+ end
31
+
32
+ def exif(path)
33
+ Exif::Data.new(path.binread)
34
+ rescue Exif::Error
35
+ nil
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ require 'erb'
2
+
3
+ module ExtractExifGps
4
+ HTML_TEMPLATE = 'templates/basic.html.erb'.freeze
5
+
6
+ # Render a {FileSet} as an HTML file
7
+ class HtmlRenderer
8
+ attr_reader :search_paths, :title, :files
9
+
10
+ # @param files [FileSet]
11
+ # @param template [StringIO,#to_s] A +StringIO+ containing the body of an
12
+ # ERB template, or the path to a template file
13
+ # @param title [String] The title to use in the rendered HTML file
14
+ # @param search_paths [Array<#to_s>] The directories searched for images
15
+ def initialize(files, template:, title: 'EXIF GPS', search_paths: [])
16
+ @files = files
17
+ @template = template
18
+ @title = title
19
+ @search_paths = search_paths
20
+ end
21
+
22
+ def to_s
23
+ erb.result(binding)
24
+ end
25
+
26
+ private
27
+
28
+ def erb
29
+ ERB.new(read_template, nil, '<>')
30
+ end
31
+
32
+ def read_template
33
+ case @template
34
+ when StringIO then @template.read
35
+ else IO.read(@template)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,97 @@
1
+ <%
2
+ require 'cgi'
3
+
4
+ ICON_GOOGLE_MAPS = <<BASE64.gsub(/\s+/, '')
5
+ iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxj
6
+ wv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAATYSURBVGhD7ZZvbBNlHMdrjLzipe/ghYEEdYkaaT
7
+ sUu3ZuEEVUZlhLIsOI4Y8GFaIuEtGpaNhERBMX7CKBLHSbTgkFIoZ0LUNZtnYtpdq1ReafwrqxP66
8
+ UjaGj5Of9zufK3fW569PZFhL7TT5Zd89zfT7f6z3Xaor5PydYUjIrYtRbwkZ9W8ioj3B/JxHyuhXH
9
+ cA6ZfmslbNJWc6L9YZMeVDHqz4XLdSvJaTc/YDbfzol/TJVVAc/Bc8nb3LyETLpdNEEWuBI7ydtkT
10
+ km7edacJsvOOVZLbG7TKsgFz769nCqWDX2m0iqiqJ65Vks9TWKm3LXHAq7li6lS2YD7xqvV3kE0lc
11
+ Nd+UG5hKFtM9hCHTA0+Sdcu56E2MQY//9DrS9L5tFY/Zb61fcYtGDX38+Dr2lzBCKmUjPRVI5cYM2
12
+ xepicvgq0XJgYhfl7ayTz5ex57lGqDHKEk95693yoXTCPB1/jMdpchNtHNqKpHPHii9tegalrf/Gy
13
+ fyQuwuudVnjmcB28cdIKZ0b6YdnBrRJZGo6n6LeP+xGtRF5cwmNYSD0nbNKdJZrKES/eGnam5O/Z9
14
+ 7xEjBXfklKKiJ67Ze5LkxfAMdo53CdwmWgqR7z4xSvjfAG88sKx6iPvww53i4SNjt2pcTmBipwWmC
15
+ KayhEvjhsWs8L+TupYo9/OHxPHPRROjctxPkm/hXDDZnsLcU+iCNFUjnhxfOpgxJ/AuuO7+CcQ8sv
16
+ 4BX7cEfWlxuXsfqGCKoPQNvHRUrVNrLcSTeWIFxf2QDQxDPfuXysRe6B5PYxcifPjDe42yZiY8oYq
17
+ qowAXm18hB7mbptexc37L31G3YNEUznixfEpJDxCz18ehtqTTVDF3U6vdX4B/fEYfxzHFx7YKJGW0
18
+ 24uowplhVHXQRTVI1+85tgOmJie4mXl+Ts5DS86PpXMlzPvy1Xwo70CIpX0zcxCyKi7GjLoFxBF9d
19
+ AkFrVsggMhBwxwX1wojV9gh86dgscPvpk2VwzKn+haChAug7HPHqbKsaGrJXqZQxOZCWJ5nr4yOP/
20
+ SIoqcOtzV/x40mtuIXubQZLIlTZ6Q9Bjg7Ar2H3ahp5dwv3+0dxI1ttCEskFJHrkUXA/+r5ohVJ55
21
+ P+Cc01/buIufZWhSrGSS93q7oNfrhsD2bVRpMT99WMfPJVrsoYmxoCafEMkj3p4u6Kt+giqO4Fhvz
22
+ 6nCFVCTn/51MwwORsHr86QKIH7bPqo8crqlOTWPaLGHJqhGJvn4+AjE43GIDcbSSgQ3rEmTx2PiOU
23
+ SLPTRJJVjlBeQl/M170wr4bfsLU0BN3uPaAp3+UYk8tYSnG0Irb+wFfI3H8l5ATd7t3AKVdWNQ+W4
24
+ CXP5LGUsEGranCgQ++kAin5cCLPLGbQkelhI++7epAr6jh/JbIBt55hKcZOgxI4SXGdPkc1pgJvIC
25
+ mUoE163mqMlfgf8iL6BW4rfPP4FA/Xv5KZALeQHWjZ2zArmUF8i2BNFiTz7lBdRLDEhKEC325FteQ
26
+ LVE7EYJosWeQsgLqJUYICWIFnsKJS+gVuL3gWj2BYYDpuuFkhdQKtE/FE0SLfZ898PSbrF8j/PVvM
27
+ oLYAmnf1xS4JufXd1Eiz2NJ0yzscRIwJTsdm0qiLwAlug4M8pfeZRvDLbPJlrFFFNMMbdUNJp/AAA
28
+ NFzA9z3URAAAAAElFTkSuQmCC
29
+ BASE64
30
+ %><!doctype html>
31
+ <html>
32
+ <head>
33
+ <meta charset="utf-8">
34
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1">
36
+ <!-- load MUI -->
37
+ <link href="http://cdn.muicss.com/mui-0.9.39/css/mui.min.css" rel="stylesheet" type="text/css" />
38
+ <title><%= title %></title>
39
+ <style>
40
+ .google-map {
41
+ background-image: url(data:image/png;base64,<%= ICON_GOOGLE_MAPS %>);
42
+ background-size: cover;
43
+ display: block;
44
+ width: 24px;
45
+ height: 24px;
46
+ }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ <!-- example content -->
51
+ <div class="mui-container">
52
+ <h1><%= title %></h1>
53
+
54
+ <div class="mui-panel">
55
+ <h2>Search Paths</h2>
56
+ <ul>
57
+ <% search_paths.each do |path| %>
58
+ <li><code><%= path %></code></li>
59
+ <% end %>
60
+ </ul>
61
+ </div>
62
+
63
+ <div class="mui-panel">
64
+ <h2>Results</h2>
65
+
66
+ <table class="mui-table">
67
+ <thead>
68
+ <tr>
69
+ <th>Path</th>
70
+ <th>Latitude</th>
71
+ <th>Longitude</th>
72
+ <th>Map</th>
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ <% files.each do |item| %>
77
+ <tr>
78
+ <td>
79
+ <a href="<%= item[:path] %>"><code><%= item[:path] %></code></a>
80
+ </td>
81
+ <td><%= item[:lat] %></td>
82
+ <td><%= item[:lon] %></td>
83
+ <td>
84
+ <% if item[:lat] && item[:lon] %>
85
+ <a class="google-map" href="https://www.google.com/maps?q=<%= CGI::escape item[:lat].to_s %>, <%= CGI::escape item[:lon].to_s %>">
86
+ </a>
87
+ <% end %>
88
+ </td>
89
+ </tr>
90
+ <% end %>
91
+ </tbody>
92
+ </table>
93
+ </div>
94
+ </div>
95
+ </body>
96
+ </html>
97
+
@@ -0,0 +1,63 @@
1
+ require 'helper'
2
+
3
+ module ExtractExifGps
4
+ class TestCoord < UnitTest
5
+ def test_from_exif__missing
6
+ assert_nil Coord.from_exif(nil)
7
+ assert_nil Coord.from_exif({})
8
+ assert_nil Coord.from_exif(gps: {})
9
+ end
10
+
11
+ def test_from_exif__present
12
+ assert_equal({lat: %{N 38°}, lon: %{W 122°} },
13
+ from_exif(gps:
14
+ {gps_latitude_ref: "N", gps_latitude: [(38/1), (0/1), (0/1)],
15
+ gps_longitude_ref: "W", gps_longitude: [(122/1), (0/1), (0/1)]}
16
+ )
17
+ )
18
+
19
+ assert_equal({lat: %{N 38° 24'}, lon: %{W 122° 49'} },
20
+ from_exif(gps:
21
+ {gps_latitude_ref: "N", gps_latitude: [(38/1), (24/1), (0/1)],
22
+ gps_longitude_ref: "W", gps_longitude: [(122/1), (1243/25), (0/1)]}
23
+ )
24
+ )
25
+
26
+ assert_equal({lat: %{N 38° 24' 10"}, lon: %{W 122° 49' 20"} },
27
+ from_exif(gps:
28
+ {gps_latitude_ref: "N", gps_latitude: [(38/1), (24/1), (0/1), (10/1)],
29
+ gps_longitude_ref: "W", gps_longitude: [(122/1), (1243/25), (0/1), (20/1)]}
30
+ )
31
+ )
32
+ end
33
+
34
+ def test_to_s__integers
35
+ assert_equal %{N 1°}, coords('N', 1).to_s
36
+ assert_equal %{N 1° 2'}, coords('N', 1, 2).to_s
37
+ assert_equal %{N 1° 2' 3"}, coords('N', 1, 2, 3).to_s
38
+ end
39
+
40
+ def test_to_s__floats_short
41
+ assert_equal %{N 0.5°}, coords('N', 1/2r).to_s
42
+ assert_equal %{N 0.5° 0.25'}, coords('N', 1/2r, 2/8r).to_s
43
+ assert_equal %{N 0.5° 0.25' 0.1875"}, coords('N', 1/2r, 2/8r, 3/16r).to_s
44
+ end
45
+
46
+ def test_to_s__floats_long
47
+ assert_equal %{N 0.33333°}, coords('N', 1/3r).to_s
48
+ assert_equal %{N 0.33333° 0.16667'}, coords('N', 1/3r, 1/6r).to_s
49
+ assert_equal %{N 0.33333° 0.16667' 0.08333"}, coords('N', 1/3r, 1/6r, 1/12r).to_s
50
+ end
51
+
52
+ private
53
+
54
+ def from_exif(data)
55
+ coords = Coord.from_exif(data)
56
+ {lat: coords[:lat].to_s, lon: coords[:lon].to_s}
57
+ end
58
+
59
+ def coords(dir, *coords)
60
+ Coord.new(dir, coords)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,33 @@
1
+ require 'helper'
2
+
3
+ module ExtractExifGps
4
+ class CsvRendererTest < UnitTest
5
+ def test_to_s__empty_set
6
+ assert_equal "Path,Latitude,Longitude\n", render([])
7
+ end
8
+
9
+ def test_to_s__simple
10
+ assert_equal "Path,Latitude,Longitude\na,b,c\n",
11
+ render([item('a', 'b', 'c')])
12
+ assert_equal "Path,Latitude,Longitude\na,b,c\n1,2,3\n",
13
+ render([item('a', 'b', 'c'), item('1', '2', '3')])
14
+ end
15
+
16
+ def test_to_s__no_gps
17
+ assert_equal "Path,Latitude,Longitude\na,,\n",
18
+ render([item('a')])
19
+ assert_equal "Path,Latitude,Longitude\na,,\n1,,\n",
20
+ render([item('a'), item('1')])
21
+ end
22
+
23
+ private
24
+
25
+ def render(set)
26
+ CsvRenderer.new(set).to_s
27
+ end
28
+
29
+ def item(*parts)
30
+ { path: parts[0], lat: parts[1], lon: parts[2] }
31
+ end
32
+ end
33
+ end
@@ -1,8 +1,10 @@
1
1
  require 'helper'
2
2
 
3
- class TestExtractExifGps < UnitTest
4
- def test_version
5
- refute_nil ExtractExifGps::Version::MAJOR
6
- refute_nil ExtractExifGps::Version::STRING
3
+ module ExtractExifGps
4
+ class TestExtractExifGps < UnitTest
5
+ def test_version
6
+ refute_nil Version::MAJOR
7
+ refute_nil Version::STRING
8
+ end
7
9
  end
8
10
  end
@@ -0,0 +1,67 @@
1
+ require 'helper'
2
+
3
+ module ExtractExifGps
4
+ class HtmlRendererTest < UnitTest
5
+ def test_to_s__empty_set
6
+ assert_equal expected_lines('TITLE: EXIF GPS', 'PATHS:', 'FILES:'),
7
+ render([])
8
+ end
9
+
10
+ def test_to_s__simple
11
+ assert_equal expected_lines('TITLE: EXIF GPS', 'PATHS:', 'FILES:', ' a|b|c'),
12
+ render([item('a', 'b', 'c')])
13
+ assert_equal expected_lines('TITLE: EXIF GPS', 'PATHS:', 'FILES:', ' a|b|c', ' 1|2|3'),
14
+ render([item('a', 'b', 'c'), item('1', '2', '3')])
15
+ end
16
+
17
+ def test_to_s__no_gps
18
+ assert_equal expected_lines('TITLE: EXIF GPS', 'PATHS:', 'FILES:', ' a||'),
19
+ render([item('a')])
20
+ assert_equal expected_lines('TITLE: EXIF GPS', 'PATHS:', 'FILES:', ' a||', ' 1||'),
21
+ render([item('a'), item('1')])
22
+ end
23
+
24
+ def test_to_s__title
25
+ assert_equal expected_lines('TITLE: TEST', 'PATHS:', 'FILES:', ' a|b|c'),
26
+ render([item('a', 'b', 'c')], title: 'TEST')
27
+ end
28
+
29
+ def test_to_s__search_path
30
+ assert_equal expected_lines('TITLE: EXIF GPS', 'PATHS:', ' path', 'FILES:', ' a|b|c'),
31
+ render([item('a', 'b', 'c')], search_paths: %w[path])
32
+ end
33
+
34
+ def test_to_s__search_paths
35
+ assert_equal expected_lines('TITLE: EXIF GPS', 'PATHS:', ' a', ' b', 'FILES:', ' a|b|c'),
36
+ render([item('a', 'b', 'c')], search_paths: %w[a b])
37
+ end
38
+
39
+ private
40
+
41
+ def render(set, args = {})
42
+ HtmlRenderer.new(set, {template: template_stub}.merge(args)).to_s.strip
43
+ end
44
+
45
+ def item(*parts)
46
+ { path: parts[0], lat: parts[1], lon: parts[2] }
47
+ end
48
+
49
+ def template_stub
50
+ StringIO.new <<ERB
51
+ TITLE: <%= title %>
52
+ PATHS:
53
+ <% search_paths.each do |path| %>
54
+ <%= path %>
55
+ <% end %>
56
+ FILES:
57
+ <% files.each do |item| %>
58
+ <%= item[:path] %>|<%= item[:lat] %>|<%= item[:lon] %>
59
+ <% end %>
60
+ ERB
61
+ end
62
+
63
+ def expected_lines(*lines)
64
+ lines.join("\n")
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ require 'helper'
2
+ require 'pathname'
3
+
4
+ module ExtractExifGps
5
+ class FileTestTest < IntegrationTest
6
+ def test_each__flat
7
+ file_set = FileSet.new('test/images')
8
+
9
+ assert_equal %w[image_a.jpg image_b.jpg image_c.jpg image_d.jpg],
10
+ file_set.each.map(&:basename).map(&:to_s).sort
11
+ end
12
+
13
+ def test_each__recursive
14
+ file_set = FileSet.new('test/images', recursive: true)
15
+
16
+ assert_equal %w[image_a.jpg image_b.jpg image_c.jpg image_d.jpg image_e.jpg],
17
+ file_set.each.map(&:basename).map(&:to_s).sort
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,33 @@
1
+ require 'helper'
2
+ require 'pathname'
3
+
4
+ module ExtractExifGps
5
+ class GpsExtractorTest < IntegrationTest
6
+ def test_to_csv
7
+ expected = <<-CSV.gsub(/^\s+/, '')
8
+ Path,Latitude,Longitude
9
+ images/image_c.jpg,N 38° 24',W 122° 49.72'
10
+ images/image_a.jpg,N 50° 5.48',W 122° 56.74'
11
+ images/image_b.jpg,,
12
+ images/image_d.jpg,,
13
+ CSV
14
+
15
+ Dir.chdir(Pathname.new(__FILE__).join('../..')) do
16
+ extractor = GpsExtractor.new(FileSet.new('images'))
17
+ assert_equal expected, CsvRenderer.new(extractor).to_s
18
+ end
19
+ end
20
+
21
+ def test_to_csv__cats
22
+ expected = <<-CSV.gsub(/^\s+/, '')
23
+ Path,Latitude,Longitude
24
+ images/cats/image_e.jpg,"N 59° 55' 29.11829""","E 10° 41' 44.15323"""
25
+ CSV
26
+
27
+ Dir.chdir(Pathname.new(__FILE__).join('../..')) do
28
+ extractor = GpsExtractor.new(FileSet.new('images/cats'))
29
+ assert_equal expected, CsvRenderer.new(extractor).to_s
30
+ end
31
+ end
32
+ end
33
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: extractexifgps
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Sangster
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-08-10 00:00:00.000000000 Z
11
+ date: 2018-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: exif
@@ -25,33 +25,33 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.2'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: thor
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.11'
34
- type: :development
33
+ version: '0.20'
34
+ type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.11'
40
+ version: '0.20'
41
41
  - !ruby/object:Gem::Dependency
42
- name: jeweler
42
+ name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 2.3.9
47
+ version: '1.11'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 2.3.9
54
+ version: '1.11'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: byebug
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -67,19 +67,19 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '8.2'
69
69
  - !ruby/object:Gem::Dependency
70
- name: yard
70
+ name: jeweler
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - "~>"
74
74
  - !ruby/object:Gem::Version
75
- version: '0.7'
75
+ version: 2.3.9
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
- version: '0.7'
82
+ version: 2.3.9
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: rdoc
85
85
  requirement: !ruby/object:Gem::Requirement
@@ -95,75 +95,89 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.12'
97
97
  - !ruby/object:Gem::Dependency
98
- name: minitest
98
+ name: yard
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '5.0'
103
+ version: '0.7'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '5.0'
110
+ version: '0.7'
111
111
  - !ruby/object:Gem::Dependency
112
- name: minitest-reporters
112
+ name: guard
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '1.1'
117
+ version: '2.14'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '1.1'
124
+ version: '2.14'
125
125
  - !ruby/object:Gem::Dependency
126
- name: simplecov
126
+ name: guard-minitest
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0.11'
131
+ version: '2.4'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0.11'
138
+ version: '2.4'
139
139
  - !ruby/object:Gem::Dependency
140
- name: guard
140
+ name: minitest
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: '2.14'
145
+ version: '5.0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '2.14'
152
+ version: '5.0'
153
153
  - !ruby/object:Gem::Dependency
154
- name: guard-minitest
154
+ name: minitest-reporters
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: '2.4'
159
+ version: '1.1'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: '2.4'
166
+ version: '1.1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.11'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.11'
167
181
  - !ruby/object:Gem::Dependency
168
182
  name: rubocop
169
183
  requirement: !ruby/object:Gem::Requirement
@@ -188,6 +202,7 @@ extra_rdoc_files:
188
202
  - README.md
189
203
  files:
190
204
  - ".document"
205
+ - ".rubocop.yml"
191
206
  - ".ruby_version"
192
207
  - Gemfile
193
208
  - Gemfile.lock
@@ -196,20 +211,28 @@ files:
196
211
  - README.md
197
212
  - Rakefile
198
213
  - bin/extractexifgps
199
- - lib/coords.rb
200
- - lib/directory_files_extractor.rb
214
+ - extractexifgps.gemspec
215
+ - lib/cli.rb
216
+ - lib/coord.rb
217
+ - lib/csv_renderer.rb
201
218
  - lib/extractexifgps.rb
202
- - test/coords_test.rb
219
+ - lib/file_set.rb
220
+ - lib/gps_extractor.rb
221
+ - lib/html_renderer.rb
222
+ - templates/basic.html.erb
223
+ - test/coord_test.rb
224
+ - test/csv_renderer_test.rb
203
225
  - test/extractexifgps_test.rb
204
226
  - test/helper.rb
205
- - test/images/.DS_Store
227
+ - test/html_renderer_test.rb
206
228
  - test/images/cats/.DS_Store
207
229
  - test/images/cats/image_e.jpg
208
230
  - test/images/image_a.jpg
209
231
  - test/images/image_b.jpg
210
232
  - test/images/image_c.jpg
211
233
  - test/images/image_d.jpg
212
- - test/integration/directory_files_extractor_test.rb
234
+ - test/integration/file_set_test.rb
235
+ - test/integration/gps_extractor_test.rb
213
236
  homepage: http://github.com/sangster/extractexifgps
214
237
  licenses:
215
238
  - MIT
@@ -225,9 +248,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
225
248
  version: '0'
226
249
  required_rubygems_version: !ruby/object:Gem::Requirement
227
250
  requirements:
228
- - - ">"
251
+ - - ">="
229
252
  - !ruby/object:Gem::Version
230
- version: 1.3.1
253
+ version: '0'
231
254
  requirements: []
232
255
  rubyforge_project:
233
256
  rubygems_version: 2.7.6
@@ -1,50 +0,0 @@
1
- require 'csv'
2
- require 'exif'
3
- require 'pathname'
4
-
5
- require_relative 'coords'
6
-
7
- module ExtractExifGps
8
- class DirectoryFilesExtractor
9
- CSV_COLUMNS = ['Path', 'Latitude', 'Longitude'].freeze
10
-
11
- def initialize(dir_path)
12
- @dir_path = dir_path
13
- end
14
-
15
- def to_csv
16
- CSV.generate do |csv|
17
- csv << CSV_COLUMNS
18
-
19
- gps_list.each do |item|
20
- csv << [ item[:path],
21
- item[:gps]&.[](:lat)&.to_s,
22
- item[:gps]&.[](:lon)&.to_s
23
- ]
24
- end
25
- end
26
- end
27
-
28
- private
29
-
30
- def gps_list
31
- exif_list.map do |item|
32
- { path: item[:path], gps: Coords.from_exif(item[:exif]) }
33
- end
34
- end
35
-
36
- def exif_list
37
- files.map { |file| { path: file, exif: exif(file) } }
38
- end
39
-
40
- def files
41
- Pathname.new(@dir_path).glob('*').select(&:file?)
42
- end
43
-
44
- def exif(path)
45
- Exif::Data.new(path.binread)
46
- rescue Exif::Error
47
- nil
48
- end
49
- end
50
- end
data/test/coords_test.rb DELETED
@@ -1,61 +0,0 @@
1
- require 'helper'
2
-
3
- class TestCoords < UnitTest
4
- def test_from_exif__missing
5
- assert_nil ExtractExifGps::Coords.from_exif(nil)
6
- assert_nil ExtractExifGps::Coords.from_exif({})
7
- assert_nil ExtractExifGps::Coords.from_exif(gps: {})
8
- end
9
-
10
- def test_from_exif__present
11
- assert_equal({lat: %{N 38°}, lon: %{W 122°} },
12
- from_exif(gps:
13
- {gps_latitude_ref: "N", gps_latitude: [(38/1), (0/1), (0/1)],
14
- gps_longitude_ref: "W", gps_longitude: [(122/1), (0/1), (0/1)]}
15
- )
16
- )
17
-
18
- assert_equal({lat: %{N 38° 24'}, lon: %{W 122° 49'} },
19
- from_exif(gps:
20
- {gps_latitude_ref: "N", gps_latitude: [(38/1), (24/1), (0/1)],
21
- gps_longitude_ref: "W", gps_longitude: [(122/1), (1243/25), (0/1)]}
22
- )
23
- )
24
-
25
- assert_equal({lat: %{N 38° 24' 10"}, lon: %{W 122° 49' 20"} },
26
- from_exif(gps:
27
- {gps_latitude_ref: "N", gps_latitude: [(38/1), (24/1), (0/1), (10/1)],
28
- gps_longitude_ref: "W", gps_longitude: [(122/1), (1243/25), (0/1), (20/1)]}
29
- )
30
- )
31
- end
32
-
33
- def test_to_s__integers
34
- assert_equal %{N 1°}, coords('N', 1).to_s
35
- assert_equal %{N 1° 2'}, coords('N', 1, 2).to_s
36
- assert_equal %{N 1° 2' 3"}, coords('N', 1, 2, 3).to_s
37
- end
38
-
39
- def test_to_s__floats_short
40
- assert_equal %{N 0.5°}, coords('N', 1/2r).to_s
41
- assert_equal %{N 0.5° 0.25'}, coords('N', 1/2r, 2/8r).to_s
42
- assert_equal %{N 0.5° 0.25' 0.1875"}, coords('N', 1/2r, 2/8r, 3/16r).to_s
43
- end
44
-
45
- def test_to_s__floats_long
46
- assert_equal %{N 0.33333°}, coords('N', 1/3r).to_s
47
- assert_equal %{N 0.33333° 0.16667'}, coords('N', 1/3r, 1/6r).to_s
48
- assert_equal %{N 0.33333° 0.16667' 0.08333"}, coords('N', 1/3r, 1/6r, 1/12r).to_s
49
- end
50
-
51
- private
52
-
53
- def from_exif(data)
54
- coords = ExtractExifGps::Coords.from_exif(data)
55
- {lat: coords[:lat].to_s, lon: coords[:lon].to_s}
56
- end
57
-
58
- def coords(dir, *coords)
59
- ExtractExifGps::Coords.new(dir, coords)
60
- end
61
- end
Binary file
@@ -1,31 +0,0 @@
1
- require 'helper'
2
- require 'pathname'
3
-
4
- class DirectoryFilesExtractorTest < IntegrationTest
5
- def test_to_csv
6
- expected = <<-CSV.gsub(/^\s+/, '')
7
- Path,Latitude,Longitude
8
- images/image_c.jpg,N 38° 24',W 122° 49.72'
9
- images/image_a.jpg,N 50° 5.48',W 122° 56.74'
10
- images/image_b.jpg,,
11
- images/image_d.jpg,,
12
- CSV
13
-
14
- Dir.chdir(Pathname.new(__FILE__).join('../..')) do
15
- assert_equal expected,
16
- ExtractExifGps::DirectoryFilesExtractor.new('images').to_csv
17
- end
18
- end
19
-
20
- def test_to_csv__cats
21
- expected = <<-CSV.gsub(/^\s+/, '')
22
- Path,Latitude,Longitude
23
- images/cats/image_e.jpg,"N 59° 55' 29.11829""","E 10° 41' 44.15323"""
24
- CSV
25
-
26
- Dir.chdir(Pathname.new(__FILE__).join('../..')) do
27
- assert_equal expected,
28
- ExtractExifGps::DirectoryFilesExtractor.new('images/cats').to_csv
29
- end
30
- end
31
- end