phtools 0.7.1 → 0.7.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a14a90150116d6d15d806bfbc9cbdee2c31529fe
4
- data.tar.gz: 1dfe0672729732f267645b94ea15fc5a46f94ece
3
+ metadata.gz: fdb79b3f95706f20dc3105e159bba0228c5b7542
4
+ data.tar.gz: 976d6852897171998919dcbb3d771f46d729ae10
5
5
  SHA512:
6
- metadata.gz: 0d2718da44ccfc5174cf1721c235905bfadae27b11714ee4e978e9389830052ecc069c1e8d61a989a0f72642146f1e995a57bf70ad6a08236b55317df383abae
7
- data.tar.gz: 0885ca474c5040e1cb52285b003b5a39375b13df8c7c5f9dc2c2ef79775a7a8aa0ac50e06926706a9ed17c197de13f8c6b832e906bae3f7fbde02dc9a18e7198
6
+ metadata.gz: 5e996378046496d338500dfb1d8c4762b8f640684469db603c84a89a29f68d1219e96341c610254de6146785e2295b13caccae6ec64a97915c03bc10587c539b
7
+ data.tar.gz: 5214e3b953ef6dfd445df54f05115adb887cb4f595fed8f57b03cbafe875afa870d0aff7fee5176895d1fcf6015fae38830891d98e7dd5250e9ebf8a44fb6fe6
data/README.md CHANGED
@@ -13,6 +13,8 @@ PHTOOLS is an instrument intended for photographers\photo enthusiasts who:
13
13
  * appreciate the use of internal metadata (EXIF, XMP etc.) beleiving it is the best way to keep context info of the picture
14
14
  * are Ok with the use of Command Line tools
15
15
 
16
+ **********
17
+
16
18
  ## Installation
17
19
  ### Install for usage
18
20
  1. Get the latest [ruby](https://www.ruby-lang.org/) (>= 2.3) installed.
@@ -35,13 +37,17 @@ bundle exec cucumber
35
37
  bundle exec guard
36
38
  ```
37
39
 
40
+ **********
41
+
38
42
  ## PHTOOLS Use cases
39
- ### Use Case 1. Collect photos, videos, raw-photos from different sources into one place (for further processing)
40
43
 
44
+ ### USE CASE 1. Collect photos, videos, raw-photos from different sources into one place (for further processing)
41
45
  #### Given
42
46
  I have copies of SD Cards with photos, videos taken with DSLR camera on my Hard Disk in `~/Desktop/SDCard1` and in `~/Desktop/SDCard2`.
43
47
 
44
- And I have empty folder `~/Desktop/assets_staging` I would like to collect all the photo-files to.
48
+ And I have empty folder `~/Desktop/assets_staging` (lets call it _working folder_).
49
+
50
+ And I want all the photo-video files from SD copies (including ones placed deep inside the folder structure of the SD card) to be moved to the _working folder_.
45
51
 
46
52
  #### When
47
53
  I run:
@@ -56,8 +62,11 @@ And all videos are moved to `~/Desktop/assets_staging/VIDEO`.
56
62
 
57
63
  And all raw photo-files are moved to `~/Desktop/assets_staging/RAW`.
58
64
 
59
- ### Use Case 2. Mass rename photos in accordance with PHTOOLS standard (and don't forget to backup before)
65
+ ==========
60
66
 
67
+ ### USE CASE 2. Renaming files in accordance with PHTOOLS standard
68
+
69
+ #### USE CASE 2.1 Mass rename photos in accordance with PHTOOLS standard (and don't forget to backup before)
61
70
  #### Given
62
71
  I have dozens of photo-files in my working folder `~/Desktop/assets_staging`.
63
72
 
@@ -75,6 +84,65 @@ I get all photos in `~/Desktop/assets_staging` renamed according to PHTOOLS stan
75
84
 
76
85
  And I have all original photo-files are backed-up to `~/Desktop/assets_staging/backup`.
77
86
 
87
+ ==========
88
+
89
+ #### USE CASE 2.2 Rename photos back to it's original names
90
+ #### Given
91
+ I have several photo files in my working folder `~/Desktop/assets_staging` renamed to PHTOOLS standard.
92
+
93
+ And I want to get all the files renamed back to it's original names (given by DSLR camera)
94
+
95
+ #### When
96
+ I run:
97
+ ```sh
98
+ cd ~/Desktop/assets_staging
99
+ phls | phrename --clean
100
+ ```
101
+
102
+ #### Then
103
+ I get all photos in `~/Desktop/assets_staging` renamed to it's original names.
104
+
105
+ ==========
106
+
107
+ #### USE CASE 2.3 Change author nickname in the filenames
108
+ #### Given
109
+ I have several photo files in my working folder `~/Desktop/assets_staging` renamed to PHTOOLS standard. Some photos were made by ANB, some photos were made by Alex (nick _ALE_)
110
+
111
+ And I want to change the author NICKNAME _ALE_ to _ALX_.
112
+
113
+ #### When
114
+ I run:
115
+ ```sh
116
+ cd ~/Desktop/assets_staging
117
+ phls '*ALE*'| phrename -a alx
118
+ ```
119
+
120
+ #### Then
121
+ I get all _ALE_ photos in `~/Desktop/assets_staging` renamed to _ALX_ nickname.
122
+
123
+ And all _ANB_ photos are kept unchanged.
124
+
125
+ _Note. `phrename` is smart enough to let the user to run it several times on one file. Every time `phrename -a` invoked it overwrites information added by previuos `phrename` run._
126
+
127
+ ==========
128
+
129
+ #### USE CASE 2.4 Adjust date-time in the filename
130
+ #### Given
131
+ I have several video files taken by iPhone in my working folder `~/Desktop/assets_staging` renamed to PHTOOLS standard.
132
+
133
+ And I've found out that iPhone gives the wrong value to CreateDate tag (in my case the error is minus 2 hours to real time of creation). Because of that 'phrename' gives the wrong _YYYYmmdd-HHMMSS_ timestamp in the filename. I want to get the correct date-time info in the names of video files.
134
+
135
+ #### When
136
+ I run:
137
+ ```sh
138
+ cd ~/Desktop/assets_staging
139
+ phls '*.mov'| phrename --shift_time 7200
140
+ ```
141
+
142
+ #### Then
143
+ I get all _mov_ files in `~/Desktop/assets_staging` renamed with correct _YYYYmmdd-HHMMSS_ timestamp (+ 7200 second = + 2 hours than it was before).
144
+
145
+ **********
78
146
 
79
147
  ## PHTOOLS concepts
80
148
  ### PHTOOLS Standard file name
data/TODO.md CHANGED
@@ -9,3 +9,8 @@
9
9
  - [ ] phmove: make options to set video, raw folder names
10
10
  - [x] phrename: add -c --clean option (based on ftclname functionality)
11
11
  - [x] phrename: add -s --shift_time option (based on ftfixdate functionality)
12
+ - [x] phrename: by-default read tags for DateTime in this order: DateTimeOriginal, DateCreated, CreateDate, DigitalCreationDate, FileModifyDate (eqv to File::mtime). 1st non-zero value will be taken as a master photo creation timestamp. It means no longer names like '00000101-000000_ANB IMG_0183.PNG' will appear.
13
+ - [x] phrename: make it safe and smart. Once the file was renamed to PHTOOL standard, re-run of phrename should not change the date-time info unless options -t or -s are used. If user wants to reset file name using exif tag - 1st clean it `phrename --clean`, then rename `phrename -a anb`
14
+ - [ ] phrename: make new usage mode: `phrename -t TAG`. Useful if user wants to re-set date-time using TAG keeping author-nickname unchanged
15
+ - [x] get rid of nesty
16
+ - [x] switch to fresh MiniExiftool gem
data/exe/phrename CHANGED
@@ -11,24 +11,38 @@ module PhTools
11
11
  ***************************************************
12
12
  phtools - *Keep Your Photos In Order* (c) ANB
13
13
  ***************************************************
14
- #{tool_name} renames the input files based on EXIF
15
- DateTimeOriginal (or CreateDate) tag and author NICKNAME provided.
16
- The target file name format is YYYYmmdd-HHMMSS_AAA ORIGINAL.EXT, where:
17
- YYYYmmdd-HHMMSS - Date-Time of photo creation (DateTimeOriginal tag),
14
+ #{tool_name} renames the input file to Standard Name:
15
+ YYYYmmdd-HHMMSS_AAA ORIGINAL.EXT, where:
16
+ YYYYmmdd-HHMMSS - Date-Time of photo creation,
18
17
  AAA - the author nickname,
19
18
  ORIGINAL.EXT - the photo name given by digital camera.
20
- Input file should be one of the types: #{file_type * ','}
19
+ By default date-time information will be taken from EXIF area as the 1st non-zero
20
+ value of the tags (shown in the order the program scans values):
21
+ EXIF:DateTimeOriginal -> IPTC:DateCreated + IPTC:TimeCreated -> XMP:DateCreated ->
22
+ -> EXIF:CreateDate -> XMP:CreateDate -> IPTC:DigitalCreationDate + IPTC:DigitalCreationTime ->
23
+ -> FileModifyDate
21
24
  Example: input file DSC03455.JPG will be renamed to 20130108-124145_ANB DSC03455.JPG
22
- #{tool_name} is optimized to be used with other *phtools* via pipes, e.g.:
23
- phls | #{tool_name} -a anb
24
25
 
25
- This program uses external utility ExifTool by Phil Harvey
26
+ Input file should be one of the supported types: #{file_type * ','}.
27
+ #{tool_name} acts as a 'filter' meaning it expects the input files to be passed
28
+ to STDIN and after the job is done it produces STDOUT with the list of renamed
29
+ files. In other words this command is intended to be used with other programs
30
+ connected via pipes, e.g.:
31
+ phls | #{tool_name} -a anb | phmove ~/targed/folder
32
+
33
+ The program is designed to be safe to re-run on the same file several times
34
+ - every re-run produces the same result (idempotent behaviour).
35
+ Once the file was renamed to Standard Name, the date-time kept in the name
36
+ is considered as a master date-time of the photo creation and will not be
37
+ changed by re-running #{tool_name} unless user explicitly sets '-t' or '-s' option.
38
+
39
+ This program uses external utility ExifTool created by Phil Harvey
26
40
  (http://www.sno.phy.queensu.ca/~phil/exiftool/).
27
41
 
28
42
  Usage:
29
- #{tool_name} -a NICK [-D] [-t TAG]
30
- #{tool_name} -c [-D]
43
+ #{tool_name} -a NICK [-t TAG] [-D]
31
44
  #{tool_name} -s DELTA [-D]
45
+ #{tool_name} -c [-D]
32
46
  #{tool_name} -h | --help
33
47
  #{tool_name} -v | --version
34
48
 
@@ -37,19 +51,20 @@ Options:
37
51
  supports only latin ASCII chars (e.g. ANB).
38
52
  No digits, no spaces, no other non-word chars allowed.
39
53
 
40
- -t TAG --tag_date=TAG Set the TAG name containing Date_Time creation
41
- info. The TAG value will be used instead of
42
- standard DateTimeOriginal tag. All existing tags you
43
- can get using command `phls filename|phmtags -f`
44
-
45
- -c --clean Rename file(s) back to original name(s)
54
+ -t TAG --tag_date=TAG Force program to use TAG as a Date-Time creation
55
+ info instead of standard phtools tags.
56
+ You can retreive all existing tags using command:
57
+ `phls filename|phmtags -f` OR
58
+ `exiftool -s filename`
46
59
 
47
60
  -s DELTA --shift_time=DELTA DELTA (in seconds) will be added to Date-Time value
48
- of the filename. If DELTA is positive the photo
61
+ kept in the name. If DELTA is positive the photo
49
62
  will become yonger: e.g. 20140720-100005, DELTA = 65
50
63
  result = 20140720-100110. If DELTA is negative the photo
51
64
  will get DELTA seconds older.
52
65
 
66
+ -c --clean Rename file back to it's original name
67
+
53
68
  -D --debug Turn on debugging (verbose) mode
54
69
  -h --help Show this screen.
55
70
  -v --version Show version.
data/lib/phbackup.rb CHANGED
@@ -36,7 +36,7 @@ module PhTools
36
36
  def process_file(phfile)
37
37
  backup_path = File.join(@backup_dir,
38
38
  phfile.basename + phfile.extname)
39
- FileUtils.cp(phfile.filename, backup_path, verbose: PhTools.debug)
39
+ FileUtils.cp(phfile.filename, backup_path, preserve: true, verbose: PhTools.debug)
40
40
  phfile
41
41
  rescue
42
42
  raise PhTools::Error, "file copying to #{@backup_dir}"
data/lib/phmove.rb CHANGED
@@ -51,7 +51,7 @@ module PhTools
51
51
  phfile_out.dirname = @target_folder
52
52
  end
53
53
 
54
- FileUtils.mv(phfile.filename, phfile_out.filename) unless phfile == phfile_out
54
+ FileUtils.mv(phfile.filename, phfile_out.filename, verbose: PhTools.debug) unless phfile == phfile_out
55
55
  phfile_out
56
56
  rescue SystemCallError => e
57
57
  raise PhTools::Error, 'file moving - ' + e.message
data/lib/phrename.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  # (c) ANB Andrew Bizyaev
4
4
 
5
5
  require 'date'
6
+ require 'mini_exiftool'
6
7
  require 'phtools/runner'
7
- require 'phtools/mini_exiftool-2.3.0anb'
8
8
 
9
9
  module PhTools
10
10
  class Phrename < Runner
@@ -32,31 +32,81 @@ module PhTools
32
32
 
33
33
  def process_file(phfile)
34
34
  phfile_out = phfile.clone
35
+ info_msg = ''
35
36
  case @mode
36
37
  when :rename
37
- begin
38
- tag = MiniExiftool.new(phfile.filename, timestamps: DateTime)
39
- rescue
40
- raise PhTools::Error, 'EXIF tags reading'
41
- end
42
- if @user_tag_date.empty?
43
- dto = tag.date_time_original || tag.create_date || PhFile::ZERO_DATE
44
- else
45
- fail PhTools::Error, "tag #{@user_tag_date} is not found" unless tag[@user_tag_date]
46
- fail PhTools::Error, "tag #{@user_tag_date} is not a DateTime type" unless tag[@user_tag_date].kind_of?(DateTime)
47
- dto = tag[@user_tag_date] || PhFile::ZERO_DATE
38
+ if phfile_out.basename_is_standard? and @user_tag_date.empty?
39
+ # change only author, keeping date-time safe
40
+ phfile_out.standardize!(author: @author)
41
+ info_msg = "'#{phfile.basename+phfile.extname}' already standard name. Keeping date-time part unchanged"
42
+ else #full rename
43
+ begin
44
+ tags = MiniExiftool.new(phfile.filename,
45
+ replace_invalid_chars: true,
46
+ composite: true,
47
+ timestamps: DateTime)
48
+ rescue
49
+ raise PhTools::Error, 'EXIF tags reading'
50
+ end
51
+ if @user_tag_date.empty?
52
+ # searching for DateTime stamp value in the tags using priority:
53
+ # EXIF:DateTimeOriginal -> IPTC:DateCreated + IPTC:TimeCreated -> XMP:DateCreated -> EXIF:CreateDate -> XMP:CreateDate -> IPTC:DigitalCreationDate + IPTC:DigitalCreationTime -> FileModifyDate
54
+ if !tags.date_time_original.nil? && tags.date_time_original.kind_of?(DateTime)
55
+ # EXIF:DateTimeOriginal or IPTC:DateCreated + IPTC:TimeCreated
56
+ dto = tags.date_time_original
57
+ tag_used = "DateTimeOriginal"
58
+
59
+ elsif !tags.date_created.nil? && tags.date_created.kind_of?(DateTime)
60
+ # XMP:DateCreated
61
+ dto = tags.date_created
62
+ tag_used = "DateCreated"
63
+
64
+ elsif !tags.create_date.nil? && tags.create_date.kind_of?(DateTime)
65
+ # EXIF:CreateDate or XMP:CreateDate
66
+ dto = tags.create_date
67
+ tag_used = "CreateDate"
68
+
69
+ elsif !tags.digital_creation_date.nil? &&
70
+ !tags.digital_creation_time.nil? &&
71
+ tags.digital_creation_date.kind_of?(String) &&
72
+ tags.digital_creation_time.kind_of?(String)
73
+ # IPTC:DigitalCreationDate + IPTC:DigitalCreationTime
74
+ dcdt = tags.digital_creation_date + " " + tags.digital_creation_time
75
+ begin
76
+ s = dcdt.sub(/^(\d+):(\d+):/, '\1-\2-')
77
+ dto = DateTime.parse(s)
78
+ rescue ArgumentError
79
+ dto = PhFile::ZERO_DATE
80
+ end
81
+ tag_used = "DigitalCreationDate + DigitalCreationTime"
82
+
83
+ else
84
+ # FileModifyDate
85
+ dto = File.mtime(phfile.filename).to_datetime
86
+ tag_used = "FileModifyDate"
87
+ end
88
+
89
+ else
90
+ # tag is set by the user
91
+ fail PhTools::Error, "tag #{@user_tag_date} is not found in a file" unless tags[@user_tag_date]
92
+ fail PhTools::Error, "tag #{@user_tag_date} is not a DateTime type" unless tags[@user_tag_date].kind_of?(DateTime)
93
+ dto = tags[@user_tag_date] || PhFile::ZERO_DATE
94
+ tag_used = "#{@user_tag_date}"
95
+ end
96
+ phfile_out.standardize!(date_time: dto, author: @author)
97
+ info_msg = "'#{phfile.basename+phfile.extname}' using tag '#{tag_used}' for date-time"
48
98
  end
49
- phfile_out.standardize!(date_time: dto, author: @author)
50
99
 
51
100
  when :clean
52
101
  phfile_out.cleanse!
53
102
 
54
103
  when :shift_time
55
- fail PhTools::Error, 'incorrect file name' unless phfile_out.basename_is_standard?
104
+ fail PhTools::Error, 'non-standard file name' unless phfile_out.basename_is_standard?
56
105
  phfile_out.standardize!(date_time: phfile_out.date_time + @shift_seconds*(1.0/86400))
57
106
  end
58
107
 
59
- FileUtils.mv(phfile.filename, phfile_out.filename) unless phfile == phfile_out
108
+ FileUtils.mv(phfile.filename, phfile_out.filename, verbose: PhTools.debug) unless phfile == phfile_out
109
+ PhTools.puts_error info_msg unless info_msg.empty?
60
110
  phfile_out
61
111
 
62
112
  rescue => e
data/lib/phtools/error.rb CHANGED
@@ -2,9 +2,6 @@
2
2
  # encoding: UTF-8
3
3
  # (c) ANB Andrew Bizyaev
4
4
 
5
- require 'nesty'
6
-
7
- # Foto tools
8
5
  module PhTools
9
6
  @debug = false
10
7
  def self.debug=(val)
@@ -15,19 +12,24 @@ module PhTools
15
12
  @debug
16
13
  end
17
14
 
15
+ def self.drill_down_error(e, level, prefix)
16
+ return if e.nil?
17
+ STDERR.puts "#{prefix}: CAUSE#{level}: #{e.class} - #{e.message}"
18
+ e.backtrace.each do |b|
19
+ STDERR.puts "#{prefix}: CAUSE#{level} BACKTRACE: #{b}"
20
+ end
21
+ drill_down_error(e.cause, level+1, prefix)
22
+ end
23
+
18
24
  def self.puts_error(msg, e = nil)
19
25
  prefix = File.basename($PROGRAM_NAME, '.rb')
20
26
  STDERR.puts "#{prefix}: #{msg}"
21
- if @debug && !e.nil?
22
- if e.respond_to?(:cause) && !e.cause.nil?
23
- STDERR.puts "#{prefix}: CAUSE: #{e.cause} - #{e.cause.message}"
24
- end
25
- e.backtrace.each do |b|
26
- STDERR.puts "#{prefix}: BACKTRACE: #{b}"
27
- end
28
- end
27
+ drill_down_error(e, 0, prefix) if @debug
28
+ end
29
+
30
+ class Error < StandardError
29
31
  end
30
32
 
31
- class Error < Nesty::NestedStandardError; end
32
- class ExiftoolTagger < Error; end
33
+ class ExiftoolTagger < Error
34
+ end
33
35
  end
@@ -51,8 +51,8 @@ module PhTools
51
51
  filename = line.chomp
52
52
  begin
53
53
  PhFile.validate_file!(filename, @file_type)
54
- ftfile = PhFile.new(filename)
55
- @os.output process_file(ftfile)
54
+ phfile = PhFile.new(filename)
55
+ @os.output process_file(phfile)
56
56
  rescue PhTools::Error => e
57
57
  PhTools.puts_error "ERROR: '#{filename}' - #{e.message}", e
58
58
  end
@@ -1,4 +1,4 @@
1
1
 
2
2
  module PhTools
3
- VERSION = '0.7.1'
3
+ VERSION = '0.7.7'
4
4
  end
data/phtools.gemspec CHANGED
@@ -47,9 +47,8 @@ Gem::Specification.new do |spec|
47
47
  spec.add_development_dependency 'rb-notifu', '>= 0'
48
48
  end
49
49
 
50
- spec.add_runtime_dependency 'nesty', '~> 1.0'
51
50
  spec.add_runtime_dependency 'docopt', '~> 0.5'
52
- # spec.add_runtime_dependency 'mini_exiftool', '~> 2.4'
51
+ spec.add_runtime_dependency 'mini_exiftool', '~> 2.8'
53
52
  spec.add_runtime_dependency 'activesupport', '~> 3.2'
54
53
  spec.add_runtime_dependency 'i18n'
55
54
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phtools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Bizyaev
@@ -179,33 +179,33 @@ dependencies:
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
181
  - !ruby/object:Gem::Dependency
182
- name: nesty
182
+ name: docopt
183
183
  requirement: !ruby/object:Gem::Requirement
184
184
  requirements:
185
185
  - - "~>"
186
186
  - !ruby/object:Gem::Version
187
- version: '1.0'
187
+ version: '0.5'
188
188
  type: :runtime
189
189
  prerelease: false
190
190
  version_requirements: !ruby/object:Gem::Requirement
191
191
  requirements:
192
192
  - - "~>"
193
193
  - !ruby/object:Gem::Version
194
- version: '1.0'
194
+ version: '0.5'
195
195
  - !ruby/object:Gem::Dependency
196
- name: docopt
196
+ name: mini_exiftool
197
197
  requirement: !ruby/object:Gem::Requirement
198
198
  requirements:
199
199
  - - "~>"
200
200
  - !ruby/object:Gem::Version
201
- version: '0.5'
201
+ version: '2.8'
202
202
  type: :runtime
203
203
  prerelease: false
204
204
  version_requirements: !ruby/object:Gem::Requirement
205
205
  requirements:
206
206
  - - "~>"
207
207
  - !ruby/object:Gem::Version
208
- version: '0.5'
208
+ version: '2.8'
209
209
  - !ruby/object:Gem::Dependency
210
210
  name: activesupport
211
211
  requirement: !ruby/object:Gem::Requirement
@@ -276,7 +276,6 @@ files:
276
276
  - lib/phtagset.rb
277
277
  - lib/phtools.rb
278
278
  - lib/phtools/error.rb
279
- - lib/phtools/mini_exiftool-2.3.0anb.rb
280
279
  - lib/phtools/ph_file.rb
281
280
  - lib/phtools/runner.rb
282
281
  - lib/phtools/utils.rb
@@ -1,703 +0,0 @@
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-2013 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 'pstore'
19
- require 'rational'
20
- require 'rbconfig'
21
- require 'set'
22
- require 'tempfile'
23
- require 'time'
24
- require 'nesty' # ANB
25
-
26
- # Simple OO access to the Exiftool command-line application.
27
- class MiniExiftool
28
-
29
- VERSION = '2.3.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, :ignore_minor_errors => false,
36
- :replace_invalid_chars => false, :timestamps => Time }
37
-
38
- # Encoding of the filesystem (filenames in command line)
39
- @@fs_enc = Encoding.find('filesystem')
40
-
41
- def self.opts_accessor *attrs
42
- attrs.each do |a|
43
- define_method a do
44
- @opts[a]
45
- end
46
- define_method "#{a}=" do |val|
47
- @opts[a] = val
48
- end
49
- end
50
- end
51
-
52
- attr_reader :filename, :errors
53
-
54
- opts_accessor :numerical, :composite, :ignore_minor_errors,
55
- :replace_invalid_chars, :timestamps
56
-
57
- @@encoding_types = %w(exif iptc xmp png id3 pdf photoshop quicktime aiff mie vorbis)
58
-
59
- def self.encoding_opt enc_type
60
- (enc_type.to_s + '_encoding').to_sym
61
- end
62
-
63
- @@encoding_types.each do |enc_type|
64
- opts_accessor encoding_opt(enc_type)
65
- end
66
-
67
- # +opts+ support at the moment
68
- # * <code>:numerical</code> for numerical values, default is +false+
69
- # * <code>:composite</code> for including composite tags while loading,
70
- # default is +true+
71
- # * <code>:ignore_minor_errors</code> ignore minor errors (See -m-option
72
- # of the exiftool command-line application, default is +false+)
73
- # * <code>:coord_format</code> set format for GPS coordinates (See
74
- # -c-option of the exiftool command-line application, default is +nil+
75
- # that means exiftool standard)
76
- # * <code>:replace_invalid_chars</code> replace string for invalid
77
- # UTF-8 characters or +false+ if no replacing should be done,
78
- # default is +false+
79
- # * <code>:timestamps</code> generating DateTime objects instead of
80
- # Time objects if set to <code>DateTime</code>, default is +Time+
81
- #
82
- # <b>ATTENTION:</b> Time objects are created using <code>Time.local</code>
83
- # therefore they use <em>your local timezone</em>, DateTime objects instead
84
- # are created <em>without timezone</em>!
85
- # * <code>:exif_encoding</code>, <code>:iptc_encoding</code>,
86
- # <code>:xmp_encoding</code>, <code>:png_encoding</code>,
87
- # <code>:id3_encoding</code>, <code>:pdf_encoding</code>,
88
- # <code>:photoshop_encoding</code>, <code>:quicktime_encoding</code>,
89
- # <code>:aiff_encoding</code>, <code>:mie_encoding</code>,
90
- # <code>:vorbis_encoding</code> to set this specific encoding (see
91
- # -charset option of the exiftool command-line application, default is
92
- # +nil+: no encoding specified)
93
- def initialize filename=nil, opts={}
94
- @opts = @@opts.merge opts
95
- if @opts[:convert_encoding]
96
- warn 'Option :convert_encoding is not longer supported!'
97
- warn 'Please use the String#encod* methods.'
98
- end
99
- @values = TagHash.new
100
- @changed_values = TagHash.new
101
- @errors = TagHash.new
102
- load filename unless filename.nil?
103
- end
104
-
105
- def initialize_from_hash hash # :nodoc:
106
- set_values hash
107
- set_opts_by_heuristic
108
- self
109
- end
110
-
111
- def initialize_from_json json # :nodoc:
112
- @output = json
113
- @errors.clear
114
- parse_output
115
- self
116
- end
117
-
118
- # Load the tags of filename.
119
- def load filename
120
- MiniExiftool.setup
121
- unless filename && File.exist?(filename)
122
- raise MiniExiftool::Error.new("File '#{filename}' does not exist.")
123
- end
124
- if File.directory?(filename)
125
- raise MiniExiftool::Error.new("'#{filename}' is a directory.")
126
- end
127
- @filename = filename
128
- @values.clear
129
- @changed_values.clear
130
- params = '-j '
131
- params << (@opts[:numerical] ? '-n ' : '')
132
- params << (@opts[:composite] ? '' : '-e ')
133
- params << (@opts[:coord_format] ? "-c \"#{@opts[:coord_format]}\"" : '')
134
- @@encoding_types.each do |enc_type|
135
- if enc_val = @opts[MiniExiftool.encoding_opt(enc_type)]
136
- params << "-charset #{enc_type}=#{enc_val} "
137
- end
138
- end
139
- if run(cmd_gen(params, @filename))
140
- parse_output
141
- else
142
- raise MiniExiftool::Error.new(@error_text)
143
- end
144
- self
145
- end
146
-
147
- # Reload the tags of an already read file.
148
- def reload
149
- load @filename
150
- end
151
-
152
- # Returns the value of a tag.
153
- def [] tag
154
- @changed_values[tag] || @values[tag]
155
- end
156
-
157
- # Set the value of a tag.
158
- def []= tag, val
159
- @changed_values[tag] = val
160
- end
161
-
162
- # Returns true if any tag value is changed or if the value of a
163
- # given tag is changed.
164
- def changed? tag=false
165
- if tag
166
- @changed_values.include? tag
167
- else
168
- !@changed_values.empty?
169
- end
170
- end
171
-
172
- # Revert all changes or the change of a given tag.
173
- def revert tag=nil
174
- if tag
175
- val = @changed_values.delete(tag)
176
- res = val != nil
177
- else
178
- res = @changed_values.size > 0
179
- @changed_values.clear
180
- end
181
- res
182
- end
183
-
184
- # Returns an array of the tags (original tag names) of the read file.
185
- def tags
186
- @values.keys.map { |key| MiniExiftool.original_tag(key) }
187
- end
188
-
189
- # Returns an array of all changed tags.
190
- def changed_tags
191
- @changed_values.keys.map { |key| MiniExiftool.original_tag(key) }
192
- end
193
-
194
- # Save the changes to the file.
195
- def save
196
- MiniExiftool.setup
197
- return false if @changed_values.empty?
198
- @errors.clear
199
- temp_file = Tempfile.new('mini_exiftool')
200
- temp_file.close
201
- temp_filename = temp_file.path
202
- FileUtils.cp filename.encode(@@fs_enc), temp_filename
203
- all_ok = true
204
- @changed_values.each do |tag, val|
205
- original_tag = MiniExiftool.original_tag(tag)
206
- arr_val = val.kind_of?(Array) ? val : [val]
207
- arr_val.map! {|e| convert_before_save(e)}
208
- params = '-q -P -overwrite_original '
209
- params << (arr_val.detect {|x| x.kind_of?(Numeric)} ? '-n ' : '')
210
- params << (@opts[:ignore_minor_errors] ? '-m ' : '')
211
- arr_val.each do |v|
212
- params << %Q(-#{original_tag}=#{escape(v)} )
213
- end
214
- result = run(cmd_gen(params, temp_filename))
215
- unless result
216
- all_ok = false
217
- @errors[tag] = @error_text.gsub(/Nothing to do.\n\z/, '').chomp
218
- end
219
- end
220
- if all_ok
221
- FileUtils.cp temp_filename, filename.encode(@@fs_enc)
222
- reload
223
- end
224
- temp_file.delete
225
- all_ok
226
- end
227
-
228
- def save!
229
- unless save
230
- err = []
231
- @errors.each do |key, value|
232
- err << "(#{key}) #{value}"
233
- end
234
- raise MiniExiftool::Error.new("MiniExiftool couldn't save. The following errors occurred: #{err.empty? ? "None" : err.join(", ")}")
235
- end
236
- end
237
-
238
- # Returns a hash of the original loaded values of the MiniExiftool
239
- # instance.
240
- def to_hash
241
- result = {}
242
- @values.each do |k,v|
243
- result[MiniExiftool.original_tag(k)] = v
244
- end
245
- result
246
- end
247
-
248
- # Returns a YAML representation of the original loaded values of the
249
- # MiniExiftool instance.
250
- def to_yaml
251
- to_hash.to_yaml
252
- end
253
-
254
- # Create a MiniExiftool instance from a hash. Default value
255
- # conversions will be applied if neccesary.
256
- def self.from_hash hash, opts={}
257
- instance = MiniExiftool.new nil, opts
258
- instance.initialize_from_hash hash
259
- instance
260
- end
261
-
262
- # Create a MiniExiftool instance from JSON data. Default value
263
- # conversions will be applied if neccesary.
264
- def self.from_json json, opts={}
265
- instance = MiniExiftool.new nil, opts
266
- instance.initialize_from_json json
267
- instance
268
- end
269
-
270
- # Create a MiniExiftool instance from YAML data created with
271
- # MiniExiftool#to_yaml
272
- def self.from_yaml yaml, opts={}
273
- MiniExiftool.from_hash YAML.load(yaml), opts
274
- end
275
-
276
- # Returns the command name of the called Exiftool application.
277
- def self.command
278
- @@cmd
279
- end
280
-
281
- # Setting the command name of the called Exiftool application.
282
- def self.command= cmd
283
- @@cmd = cmd
284
- end
285
-
286
- # Returns the options hash.
287
- def self.opts
288
- @@opts
289
- end
290
-
291
- # Returns a set of all known tags of Exiftool.
292
- def self.all_tags
293
- unless defined? @@all_tags
294
- @@all_tags = pstore_get :all_tags
295
- end
296
- @@all_tags
297
- end
298
-
299
- # Returns a set of all possible writable tags of Exiftool.
300
- def self.writable_tags
301
- unless defined? @@writable_tags
302
- @@writable_tags = pstore_get :writable_tags
303
- end
304
- @@writable_tags
305
- end
306
-
307
- # Returns the original Exiftool name of the given tag
308
- def self.original_tag tag
309
- unless defined? @@all_tags_map
310
- @@all_tags_map = pstore_get :all_tags_map
311
- end
312
- @@all_tags_map[tag]
313
- end
314
-
315
- # Returns the version of the Exiftool command-line application.
316
- def self.exiftool_version
317
- output = `#{MiniExiftool.command} -ver 2>&1`
318
- unless $?.exitstatus == 0
319
- raise MiniExiftool::Error.new("Command '#{MiniExiftool.command}' not found")
320
- end
321
- output.chomp!
322
- end
323
-
324
- def self.unify tag
325
- tag.to_s.gsub(/[-_]/,'').downcase
326
- end
327
-
328
- # Exception class
329
- class MiniExiftool::Error < Nesty::NestedStandardError; end # ANB
330
-
331
- ############################################################################
332
- private
333
- ############################################################################
334
-
335
- @@setup_done = false
336
- def self.setup
337
- return if @@setup_done
338
- @@error_file = Tempfile.new 'errors'
339
- @@error_file.close
340
- @@setup_done = true
341
- end
342
-
343
- def cmd_gen arg_str='', filename
344
- [@@cmd, arg_str.encode('UTF-8'), escape(filename.encode(@@fs_enc))].map {|s| s.force_encoding('UTF-8')}.join(' ')
345
- end
346
-
347
- def run cmd
348
- if $DEBUG
349
- $stderr.puts cmd
350
- end
351
- @output = `#{cmd} 2>#{@@error_file.path}`
352
- @status = $?
353
- unless @status.exitstatus == 0
354
- @error_text = File.readlines(@@error_file.path).join
355
- @error_text.force_encoding('UTF-8')
356
- return false
357
- else
358
- @error_text = ''
359
- return true
360
- end
361
- end
362
-
363
- def convert_before_save val
364
- case val
365
- when Time
366
- val = val.strftime('%Y:%m:%d %H:%M:%S')
367
- end
368
- val
369
- end
370
-
371
- def method_missing symbol, *args
372
- tag_name = symbol.id2name
373
- if tag_name.sub!(/=$/, '')
374
- self[tag_name] = args.first
375
- else
376
- self[tag_name]
377
- end
378
- end
379
-
380
- def parse_output
381
- adapt_encoding
382
- set_values JSON.parse(@output).first
383
- end
384
-
385
- def adapt_encoding
386
- @output.force_encoding('UTF-8')
387
- if @opts[:replace_invalid_chars] && !@output.valid_encoding?
388
- @output.encode!('UTF-16le', invalid: :replace, replace: @opts[:replace_invalid_chars]).encode!('UTF-8')
389
- end
390
- end
391
-
392
- def convert_after_load tag, value
393
- return value unless value.kind_of?(String)
394
- return value unless value.valid_encoding?
395
- case value
396
- when /^\d{4}:\d\d:\d\d \d\d:\d\d:\d\d/
397
- s = value.sub(/^(\d+):(\d+):/, '\1-\2-')
398
- begin
399
- if @opts[:timestamps] == Time
400
- value = Time.parse(s)
401
- elsif @opts[:timestamps] == DateTime
402
- value = DateTime.parse(s)
403
- else
404
- raise MiniExiftool::Error.new("Value #{@opts[:timestamps]} not allowed for option timestamps.")
405
- end
406
- rescue ArgumentError
407
- value = false
408
- end
409
- when /^\+\d+\.\d+$/
410
- value = value.to_f
411
- when /^0+[1-9]+$/
412
- # nothing => String
413
- when /^-?\d+$/
414
- value = value.to_i
415
- when %r(^(\d+)/(\d+)$)
416
- value = Rational($1.to_i, $2.to_i)
417
- when /^[\d ]+$/
418
- # nothing => String
419
- end
420
- value
421
- end
422
-
423
- def set_values hash
424
- hash.each_pair do |tag,val|
425
- @values[tag] = convert_after_load(tag, val)
426
- end
427
- # Remove filename specific tags use attr_reader
428
- # MiniExiftool#filename instead
429
- # Cause: value of tag filename and attribute
430
- # filename have different content, the latter
431
- # holds the filename with full path (like the
432
- # sourcefile tag) and the former the basename
433
- # of the filename also there is no official
434
- # "original tag name" for sourcefile
435
- %w(directory filename sourcefile).each do |t|
436
- @values.delete(t)
437
- end
438
- end
439
-
440
- def set_opts_by_heuristic
441
- @opts[:composite] = tags.include?('ImageSize')
442
- @opts[:numerical] = self.file_size.kind_of?(Integer)
443
- @opts[:timestamps] = self.FileModifyDate.kind_of?(DateTime) ? DateTime : Time
444
- end
445
-
446
- def self.pstore_get attribute
447
- load_or_create_pstore unless defined? @@pstore
448
- result = nil
449
- @@pstore.transaction(true) do |ps|
450
- result = ps[attribute]
451
- end
452
- result
453
- end
454
-
455
- @@running_on_windows = /mswin|mingw|cygwin/ === RbConfig::CONFIG['host_os']
456
-
457
- def self.load_or_create_pstore
458
- # This will hopefully work on *NIX and Windows systems
459
- home = ENV['HOME'] || ENV['HOMEDRIVE'] + ENV['HOMEPATH'] || ENV['USERPROFILE']
460
- subdir = @@running_on_windows ? '_mini_exiftool' : '.mini_exiftool'
461
- FileUtils.mkdir_p(File.join(home, subdir))
462
- pstore_filename = File.join(home, subdir, 'exiftool_tags_' << exiftool_version.gsub('.', '_') << '.pstore')
463
- @@pstore = PStore.new pstore_filename
464
- if !File.exist?(pstore_filename) || File.size(pstore_filename) == 0
465
- @@pstore.transaction do |ps|
466
- ps[:all_tags] = all_tags = determine_tags('list')
467
- ps[:writable_tags] = determine_tags('listw')
468
- map = {}
469
- all_tags.each { |k| map[unify(k)] = k }
470
- ps[:all_tags_map] = map
471
- end
472
- end
473
- end
474
-
475
- def self.determine_tags arg
476
- output = `#{@@cmd} -#{arg}`
477
- lines = output.split(/\n/)
478
- tags = Set.new
479
- lines.each do |line|
480
- next unless line =~ /^\s/
481
- tags |= line.chomp.split
482
- end
483
- tags
484
- end
485
-
486
- if @@running_on_windows
487
- def escape val
488
- '"' << val.to_s.gsub(/([\\"])/, "\\\\\\1") << '"'
489
- end
490
- else
491
- def escape val
492
- '"' << val.to_s.gsub(/([\\"$])/, "\\\\\\1") << '"'
493
- end
494
- end
495
-
496
- # Hash with indifferent access:
497
- # DateTimeOriginal == datetimeoriginal == date_time_original
498
- class TagHash < Hash # :nodoc:
499
- def[] k
500
- super(unify(k))
501
- end
502
- def []= k, v
503
- super(unify(k), v)
504
- end
505
- def delete k
506
- super(unify(k))
507
- end
508
-
509
- def unify tag
510
- MiniExiftool.unify tag
511
- end
512
- end
513
- end
514
-
515
- # ANB - dump of real @values:
516
- # exiftoolversion=9.41
517
- # filesize=128 kB
518
- # filemodifydate=2014-04-02T23:00:50+04:00
519
- # fileaccessdate=2014-04-02T23:00:51+04:00
520
- # fileinodechangedate=2014-04-02T23:00:50+04:00
521
- # filepermissions=rw-r--r--
522
- # filetype=JPEG
523
- # mimetype=image/jpeg
524
- # jfifversion=1.01
525
- # exifbyteorder=Little-endian (Intel, II)
526
- # imagedescription=
527
- # make=SONY
528
- # model=SLT-A65V
529
- # xresolution=350
530
- # yresolution=350
531
- # resolutionunit=inches
532
- # software=SLT-A65V v1.05
533
- # modifydate=2014-04-02T20:50:32+00:00
534
- # artist=Andrey Bizyaev (photographer); Andrey Bizyaev (camera owner)
535
- # ycbcrpositioning=Co-sited
536
- # copyright=2013 (c) Andrey Bizyaev. All Rights Reserved.
537
- # exposuretime=1/100
538
- # fnumber=5.6
539
- # exposureprogram=Program AE
540
- # iso=160
541
- # sensitivitytype=Recommended Exposure Index
542
- # recommendedexposureindex=160
543
- # exifversion=230
544
- # datetimeoriginal=2013-01-03T15:39:08+00:00
545
- # createdate=2013-01-03T15:39:08+00:00
546
- # componentsconfiguration=Y, Cb, Cr, -
547
- # compressedbitsperpixel=2
548
- # brightnessvalue=6.3375
549
- # exposurecompensation=0
550
- # maxaperturevalue=5.6
551
- # meteringmode=Multi-segment
552
- # lightsource=Unknown
553
- # flash=Off, Did not fire
554
- # focallength=55.0 mm
555
- # quality=Fine
556
- # flashexposurecomp=0
557
- # teleconverter=None
558
- # whitebalancefinetune=0
559
- # rating=0
560
- # brightness=0
561
- # longexposurenoisereduction=On (unused)
562
- # highisonoisereduction=Normal
563
- # hdr=Off; Uncorrected image
564
- # multiframenoisereduction=Off
565
- # pictureeffect=Off
566
- # softskineffect=Off
567
- # vignettingcorrection=Auto
568
- # lateralchromaticaberration=Auto
569
- # distortioncorrection=Off
570
- # wbshiftabgm=0 0
571
- # faceinfooffset=94
572
- # sonydatetime=2013-01-03T15:39:08+00:00
573
- # sonyimagewidth=6000
574
- # facesdetected=0
575
- # faceinfolength=37
576
- # metaversion=DC7303320222000
577
- # maxaperture=5.3
578
- # minaperture=33
579
- # flashstatus=Built-in Flash present
580
- # imagecount=8330
581
- # lensmount=A-Mount
582
- # lensformat=APS-C
583
- # sequenceimagenumber=1
584
- # sequencefilenumber=1
585
- # releasemode2=Normal
586
- # shotnumbersincepowerup=2
587
- # sequencelength=1 shot
588
- # cameraorientation=Horizontal (normal)
589
- # quality2=JPEG
590
- # sonyimageheight=4000
591
- # modelreleaseyear=2011
592
- # batterylevel=18%
593
- # afpointsselected=(all)
594
- # fileformat=ARW 2.3
595
- # sonymodelid=SLT-A65 / SLT-A65V
596
- # creativestyle=Standard
597
- # colortemperature=Auto
598
- # colorcompensationfilter=0
599
- # scenemode=Auto
600
- # zonematching=ISO Setting Used
601
- # dynamicrangeoptimizer=Auto
602
- # imagestabilization=On
603
- # lenstype=Sony DT 16-105mm F3.5-5.6 (SAL16105)
604
- # colormode=Standard
605
- # lensspec=DT 16-105mm F3.5-5.6
606
- # fullimagesize=6000x4000
607
- # previewimagesize=1616x1080
608
- # flashlevel=Normal
609
- # releasemode=Normal
610
- # sequencenumber=Single
611
- # antiblur=On (Shooting)
612
- # intelligentauto=On
613
- # whitebalance=Auto
614
- # usercomment=
615
- # flashpixversion=100
616
- # colorspace=sRGB
617
- # exifimagewidth=800
618
- # exifimageheight=534
619
- # interopindex=R98 - DCF basic file (sRGB)
620
- # interopversion=100
621
- # relatedimagewidth=6000
622
- # relatedimageheight=4000
623
- # filesource=Digital Camera
624
- # scenetype=Directly photographed
625
- # customrendered=Normal
626
- # exposuremode=Auto
627
- # focallengthin35mmformat=82 mm
628
- # scenecapturetype=Standard
629
- # contrast=Normal
630
- # saturation=Normal
631
- # sharpness=Normal
632
- # imageuniqueid=20140402-205030-0001
633
- # lensinfo=16-105mm f/3.5-5.6
634
- # lensmodel=DT 16-105mm F3.5-5.6
635
- # gpsversionid=2.3.0.0
636
- # gpslatituderef=North
637
- # gpslongituderef=East
638
- # gpsaltituderef=Above Sea Level
639
- # gpstimestamp=11:39:09.588
640
- # gpsstatus=Measurement Active
641
- # gpsmeasuremode=3-Dimensional Measurement
642
- # gpsdop=2.0026
643
- # gpsspeedref=km/h
644
- # gpsspeed=1.097
645
- # gpstrackref=True North
646
- # gpstrack=357.15
647
- # gpsmapdatum=WGS-84
648
- # gpsdatestamp=2013:01:03
649
- # gpsdifferential=No Correction
650
- # printimversion=300
651
- # compression=JPEG (old-style)
652
- # orientation=Horizontal (normal)
653
- # thumbnailoffset=21840
654
- # thumbnaillength=5859
655
- # xmptoolkit=Image::ExifTool 9.41
656
- # location=Дворцовая пл.
657
- # locationshowncity=Санкт-Петербург
658
- # locationshowncountrycode=RU
659
- # locationshowncountryname=Russia
660
- # locationshownprovincestate=Санкт-Петербург
661
- # locationshownsublocation=Дворцовая пл.
662
- # locationshownworldregion=Europe
663
- # creator=["Andrey Bizyaev (photographer)", "Andrey Bizyaev (camera owner)"]
664
- # rights=2013 (c) Andrey Bizyaev. All Rights Reserved.
665
- # subject=["before-what-travel", "before-who-Andrew", "before-where-Baltic", "before-when-day", "before-why-vacation", "before-how-fine", "before-method-digicam"]
666
- # collectionname=S-Peterburg Travel
667
- # collectionuri=anblab.net
668
- # country=Russia
669
- # state=Санкт-Петербург
670
- # iptcdigest=1569c0bffab4b64cb1107134254cf97d
671
- # currentiptcdigest=7be9172b29568717f9bc0976c93ba53d
672
- # codedcharacterset=UTF8
673
- # enveloperecordversion=4
674
- # keywords=["before-what-travel", "before-who-Andrew", "before-where-Baltic", "before-when-day", "before-why-vacation", "before-how-fine", "before-method-digicam"]
675
- # byline=["Andrey Bizyaev (photographer)", "Andrey Bizyaev (camera owner)"]
676
- # city=Санкт-Петербург
677
- # sublocation=Дворцовая пл.
678
- # provincestate=Санкт-Петербург
679
- # countryprimarylocationname=Russia
680
- # copyrightnotice=2013 (c) Andrey Bizyaev. All Rights Reserved.
681
- # applicationrecordversion=4
682
- # imagewidth=800
683
- # imageheight=534
684
- # encodingprocess=Baseline DCT, Huffman coding
685
- # bitspersample=8
686
- # colorcomponents=3
687
- # ycbcrsubsampling=YCbCr4:4:4 (1 1)
688
- # aperture=5.6
689
- # gpsaltitude=0.5 m Above Sea Level
690
- # gpsdatetime=2013-01-03T11:39:09+00:00
691
- # gpslatitude=60 deg 0' 0.00" N
692
- # gpslongitude=25 deg 0' 0.00" E
693
- # gpsposition=60 deg 0' 0.00" N, 25 deg 0' 0.00" E
694
- # imagesize=800x534
695
- # lensid=Sony DT 16-105mm F3.5-5.6 (SAL16105)
696
- # scalefactor35efl=1.5
697
- # shutterspeed=1/100
698
- # thumbnailimage=(Binary data 5859 bytes)
699
- # circleofconfusion=0.020 mm
700
- # fov=24.8 deg
701
- # focallength35efl=55.0 mm (35 mm equivalent: 82.0 mm)
702
- # hyperfocaldistance=26.80 m
703
- # lightvalue=10.9