extractexifgps 0.1.0.alpha → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +35 -0
- data/Gemfile +5 -4
- data/Gemfile.lock +1 -0
- data/README.md +56 -5
- data/Rakefile +21 -9
- data/bin/extractexifgps +1 -4
- data/extractexifgps.gemspec +109 -0
- data/lib/cli.rb +58 -0
- data/lib/{coords.rb → coord.rb} +12 -4
- data/lib/csv_renderer.rb +23 -0
- data/lib/extractexifgps.rb +5 -4
- data/lib/file_set.rb +28 -0
- data/lib/gps_extractor.rb +38 -0
- data/lib/html_renderer.rb +39 -0
- data/templates/basic.html.erb +97 -0
- data/test/coord_test.rb +63 -0
- data/test/csv_renderer_test.rb +33 -0
- data/test/extractexifgps_test.rb +6 -4
- data/test/html_renderer_test.rb +67 -0
- data/test/integration/file_set_test.rb +20 -0
- data/test/integration/gps_extractor_test.rb +33 -0
- metadata +57 -34
- data/lib/directory_files_extractor.rb +0 -50
- data/test/coords_test.rb +0 -61
- data/test/images/.DS_Store +0 -0
- data/test/integration/directory_files_extractor_test.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7142bb7da371c0971216e138a8ca090320a7dc7117426fbc93c237fd9d59d321
|
4
|
+
data.tar.gz: 176e49db29381207153297d529f259b5ce4b9e2029eaceefdb2cbaaa21db32c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
-
|
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
|
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 = %
|
22
|
-
gem.description = %
|
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 = %
|
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'] =
|
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
@@ -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
|
data/lib/{coords.rb → coord.rb}
RENAMED
@@ -1,15 +1,17 @@
|
|
1
1
|
module ExtractExifGps
|
2
|
-
|
3
|
-
|
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,
|
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(' ')}"
|
data/lib/csv_renderer.rb
ADDED
@@ -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
|
data/lib/extractexifgps.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
module ExtractExifGps
|
2
2
|
module Version
|
3
|
-
MAJOR =
|
4
|
-
MINOR =
|
3
|
+
MAJOR = 1
|
4
|
+
MINOR = 0
|
5
5
|
PATCH = 0
|
6
|
-
BUILD =
|
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 '
|
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
|
+
|
data/test/coord_test.rb
ADDED
@@ -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
|
data/test/extractexifgps_test.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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:
|
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-
|
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:
|
28
|
+
name: thor
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
34
|
-
type: :
|
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: '
|
40
|
+
version: '0.20'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
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:
|
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:
|
70
|
+
name: jeweler
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
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:
|
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:
|
98
|
+
name: yard
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: '
|
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: '
|
110
|
+
version: '0.7'
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
112
|
+
name: guard
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: '
|
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: '
|
124
|
+
version: '2.14'
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
|
-
name:
|
126
|
+
name: guard-minitest
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: '
|
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: '
|
138
|
+
version: '2.4'
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
|
-
name:
|
140
|
+
name: minitest
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
142
142
|
requirements:
|
143
143
|
- - "~>"
|
144
144
|
- !ruby/object:Gem::Version
|
145
|
-
version: '
|
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: '
|
152
|
+
version: '5.0'
|
153
153
|
- !ruby/object:Gem::Dependency
|
154
|
-
name:
|
154
|
+
name: minitest-reporters
|
155
155
|
requirement: !ruby/object:Gem::Requirement
|
156
156
|
requirements:
|
157
157
|
- - "~>"
|
158
158
|
- !ruby/object:Gem::Version
|
159
|
-
version: '
|
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: '
|
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
|
-
-
|
200
|
-
- lib/
|
214
|
+
- extractexifgps.gemspec
|
215
|
+
- lib/cli.rb
|
216
|
+
- lib/coord.rb
|
217
|
+
- lib/csv_renderer.rb
|
201
218
|
- lib/extractexifgps.rb
|
202
|
-
-
|
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/
|
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/
|
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:
|
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
|
data/test/images/.DS_Store
DELETED
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
|