phtools 0.7.1 → 0.7.7

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
  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