phtools 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -1
- data/README.md +41 -19
- data/TODO.md +5 -2
- data/exe/phmove +7 -7
- data/exe/phrename +49 -0
- data/lib/phmove.rb +22 -16
- data/lib/phrename.rb +33 -1
- data/lib/phtools/mini_exiftool-2.3.0anb.rb +703 -0
- data/lib/phtools/ph_file.rb +1 -1
- data/lib/phtools/runner.rb +2 -2
- data/lib/phtools/version.rb +1 -1
- data/lib/phtools.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f645b7d7f323aba0cea04b8a640b3d84d383b20
|
4
|
+
data.tar.gz: 0ccb3bb5953014a8cf1f194f35d62be78a6a5828
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eec57813519d3cc5f553f92f8e867101e52e218a09bd9396f874b175ad06616555c83c67f09c036b835ff967f50393b239e61e5882ec3692f5997b8cee0b4de7
|
7
|
+
data.tar.gz: f4b0f09eb7f01aafb9bef15af263117e442799a6899baa4b5cd8b5e35bd71e3c43c7ad672050418b43f620084edc3e8646c1be68425c6730700d2c18fdbd48c2
|
data/.rspec
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
--format
|
1
|
+
--format progress
|
2
2
|
--color
|
data/README.md
CHANGED
@@ -1,22 +1,31 @@
|
|
1
1
|
[![Version ](https://img.shields.io/gem/v/phtools.svg?style=flat)](https://rubygems.org/gems/phtools)
|
2
2
|
# PHTOOLS by ANB
|
3
|
-
A bundle of small CLI tools for arranging, renaming, tagging of the photo and video files. Helps keep photo-video assets in order.
|
3
|
+
A bundle of small CLI tools for arranging, renaming, tagging of the photo and video files. Helps to keep photo-video assets in order.
|
4
4
|
|
5
|
-
##
|
5
|
+
## Rationale
|
6
|
+
PHTOOLS is an instrument intended for photographers\photo enthusiasts who:
|
7
|
+
* own tons of photo-video files and want to keep it in order
|
8
|
+
* really don't like the way how digital cameras name the files: P1193691.JPG, IMP_1409.JPG, _DSC1459.ARW etc.
|
9
|
+
* for photo storage prefer usage of traditional File System (folder structure) instead of "black box" databases of media managers (like iPhoto, Photoshop etc.)
|
10
|
+
* would like to have date-time-original info in the name of the file
|
11
|
+
* expects that sorting folder content "by name" will arrange photo-video assets in chronological order
|
12
|
+
* for some events (wedding, holydays etc.) have photos from different authors and would like to keep visible author name (nik) in the file name
|
13
|
+
* appreciate the use of internal metadata (EXIF, XMP etc.) beleiving it is the best way to keep context info of the picture
|
14
|
+
* are Ok with the use of Command Line tools
|
15
|
+
|
16
|
+
## Installation
|
6
17
|
### Install for usage
|
7
|
-
Get the latest [ruby](https://www.ruby-lang.org/) (>= 2.3) installed.
|
18
|
+
1. Get the latest [ruby](https://www.ruby-lang.org/) (>= 2.3) installed.
|
19
|
+
2. Install ExifTool by Phil Harvey (http://www.sno.phy.queensu.ca/~phil/exiftool/).
|
20
|
+
3. `gem install phtools`
|
21
|
+
4. Get list of phtools: `phtools`
|
22
|
+
5. Get usage info for particular command: `phls -h`
|
8
23
|
|
9
|
-
Install ExifTool by Phil Harvey (http://www.sno.phy.queensu.ca/~phil/exiftool/)
|
10
|
-
```
|
11
|
-
gem install phtools
|
12
|
-
```
|
13
24
|
### Install for development
|
14
|
-
Fork or download from GitHub.
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
```
|
19
|
-
Develop, test:
|
25
|
+
1. Fork or download from GitHub.
|
26
|
+
2. Install dependencies: `bundle install`
|
27
|
+
3. Develop.
|
28
|
+
4. Test:
|
20
29
|
```sh
|
21
30
|
bundle exec rspec
|
22
31
|
bundle exec cucumber
|
@@ -29,21 +38,34 @@ bundle exec guard
|
|
29
38
|
## PHTOOLS Use cases
|
30
39
|
### Use Case 1. Collect photos, videos, raw-photos from different sources into one place (for further processing)
|
31
40
|
|
32
|
-
####Given
|
33
|
-
I have copies of SD Cards with photos, videos taken with DSLR camera on my Hard Disk in `~/
|
41
|
+
#### Given
|
42
|
+
I have copies of SD Cards with photos, videos taken with DSLR camera on my Hard Disk in `~/Desktop/SDCard1` and in `~/Desktop/SDCard2`.
|
34
43
|
|
35
44
|
And I have empty folder `~/Desktop/assets_staging` I would like to collect all the photo-files to.
|
36
45
|
|
37
|
-
####When
|
46
|
+
#### When
|
38
47
|
I run:
|
39
48
|
```sh
|
40
|
-
|
41
|
-
phls -R ~/path/to/copy/SDCard1 ~/path/to/copy/SDCard2 | phmove -a
|
49
|
+
phls -R ~/Desktop/SDCard1 ~/Desktop/SDCard2 | phmove -a ~/Desktop/assets_staging
|
42
50
|
```
|
43
51
|
|
44
|
-
####Then
|
52
|
+
#### Then
|
45
53
|
I get all photos moved to `~/Desktop/assets_staging`.
|
46
54
|
|
47
55
|
And all videos are moved to `~/Desktop/assets_staging/VIDEO`.
|
48
56
|
|
49
57
|
And all raw photo-files are moved to `~/Desktop/assets_staging/RAW`.
|
58
|
+
|
59
|
+
## PHTOOLS concepts
|
60
|
+
### PHTOOLS Standard file name
|
61
|
+
PHTOOLS standard file name looks like this: **`YYYYmmdd-HHMMSS_AAA ORIGINAL.EXT`**, where
|
62
|
+
|
63
|
+
**YYYYmmdd-HHMMSS** - photo creation datestamp (year-month-day-hours-minutes-seconds). By default PHTOOLS use the value of EXIF tag `DateTimeOriginal` or `CreateDate` for this purpose.
|
64
|
+
|
65
|
+
**AAA** - author nikname. 3 character long, only latin alphabet supported.
|
66
|
+
|
67
|
+
**ORIGINAL.EXT** - original file name, created by digital camera.
|
68
|
+
|
69
|
+
For example, the digital camera photo file `P1193691.JPG`, taken by AndrewBiz (aka ANB), after PHTOOLS processing will look like:
|
70
|
+
`20160902-174939_ANB P1193691.JPG`
|
71
|
+
|
data/TODO.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
- [x] core - runner.rb: print class instance variables in debug mode
|
2
2
|
- [x] phls: use init method to initialize variables
|
3
3
|
- [x] phls: change -r to -R
|
4
|
+
- [ ] phls: make it work with .folders (like ftls did)
|
4
5
|
- [x] phmove: create phmove tool based on ftarrange code (see ftools repo)
|
5
|
-
- [
|
6
|
-
- [
|
6
|
+
- [x] phmove: make target_folder as parameter not an option
|
7
|
+
- [x] phmove: -a (--arrange) parameter means to put photo, video, raw files into separate folders inside target. If -a is not set all files are moved to root of target directory (plain collection of files)
|
8
|
+
- [ ] phmove: delete unused empty RAW and VIDEO folders
|
9
|
+
- [ ] phmove: make options to set video, raw folder names
|
data/exe/phmove
CHANGED
@@ -11,22 +11,22 @@ module PhTools
|
|
11
11
|
***************************************************
|
12
12
|
phtools - *Keep Your Photos In Order* (c) ANB
|
13
13
|
***************************************************
|
14
|
-
#{tool_name} moves input file(s) into
|
15
|
-
|
16
|
-
subfolders.
|
14
|
+
#{tool_name} moves input file(s) into TARGET_FOLDER
|
15
|
+
If --arrange option is set it separates photo files, RAW photo files and VIDEO files
|
16
|
+
to corresponding subfolders.
|
17
17
|
phtools friendly files: #{file_type * ','}
|
18
18
|
|
19
19
|
Optimized to be used with other *phtools* via pipes.
|
20
|
-
Example: phls | phrename -a anb | #{tool_name}
|
20
|
+
Example: phls | phrename -a anb | #{tool_name} -a target/folder
|
21
21
|
|
22
22
|
Usage:
|
23
|
-
#{tool_name} [-
|
23
|
+
#{tool_name} [-D] [-a] TARGET_FOLDER
|
24
24
|
#{tool_name} -h | --help
|
25
25
|
#{tool_name} -v | --version
|
26
26
|
|
27
27
|
Options:
|
28
|
-
-
|
29
|
-
|
28
|
+
-a --arrange Move photos to TARGET_FOLDER, videos to TARGET_FOLDER/VIDEO
|
29
|
+
raw-files to TARGET_FOLDER/RAW
|
30
30
|
-D --debug Turn on debugging (verbose) mode
|
31
31
|
-h --help Show this screen.
|
32
32
|
-v --version Show version.
|
data/exe/phrename
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
# (c) ANB Andrew Bizyaev
|
4
|
+
|
5
|
+
module PhTools
|
6
|
+
tool_name = File.basename(__FILE__)
|
7
|
+
require "#{tool_name}"
|
8
|
+
|
9
|
+
file_type = FILE_TYPE_IMAGE + FILE_TYPE_VIDEO + FILE_TYPE_AUDIO
|
10
|
+
usage = <<DOCOPT
|
11
|
+
***************************************************
|
12
|
+
phtools - *Keep Your Photos In Order* (c) ANB
|
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),
|
18
|
+
AAA - the author nickname,
|
19
|
+
ORIGINAL.EXT - the photo name given by digital camera.
|
20
|
+
Input file should be one of the types: #{file_type * ','}
|
21
|
+
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
|
+
This program uses external utility ExifTool by Phil Harvey
|
26
|
+
(http://www.sno.phy.queensu.ca/~phil/exiftool/). Make sure you have exiftool installed.
|
27
|
+
|
28
|
+
Usage:
|
29
|
+
#{tool_name} -a NICKNAME [-D] [-t TAG]
|
30
|
+
#{tool_name} -h | --help
|
31
|
+
#{tool_name} -v | --version
|
32
|
+
|
33
|
+
Options:
|
34
|
+
-a NICKNAME --author=NICKNAME Author nickname size should be #{PhFile::NICKNAME_SIZE} chars,
|
35
|
+
have no spaces and other non-word chars,
|
36
|
+
have no digits,
|
37
|
+
have only ASCII chars (e.g. ANB)
|
38
|
+
-t TAG --tag_date=TAG Set the TAG name containing Date_Time creation
|
39
|
+
info. The TAG value will be used instead of
|
40
|
+
standard DateTimeOriginal tag.
|
41
|
+
All existing tags and tag names you can get
|
42
|
+
using command `phls filename|phmtags -f`
|
43
|
+
-D --debug Turn on debugging (verbose) mode
|
44
|
+
-h --help Show this screen.
|
45
|
+
-v --version Show version.
|
46
|
+
DOCOPT
|
47
|
+
|
48
|
+
PhTools.const_get(tool_name.capitalize).new(usage, file_type).run!
|
49
|
+
end
|
data/lib/phmove.rb
CHANGED
@@ -7,43 +7,49 @@ require 'phtools/runner'
|
|
7
7
|
module PhTools
|
8
8
|
class Phmove < Runner
|
9
9
|
def self.about
|
10
|
-
"moves input files
|
10
|
+
"moves input files to target folder"
|
11
11
|
end
|
12
12
|
private
|
13
13
|
|
14
14
|
def validate_options
|
15
|
-
@
|
16
|
-
@
|
17
|
-
|
15
|
+
@target_folder = @options_cli['TARGET_FOLDER']
|
16
|
+
@arrange = @options_cli['--arrange']
|
17
|
+
if @arrange
|
18
|
+
@raw_folder = File.join(@target_folder, 'RAW')
|
19
|
+
@video_folder = File.join(@target_folder, 'VIDEO')
|
20
|
+
else
|
21
|
+
@raw_folder = @target_folder
|
22
|
+
@video_folder = @target_folder
|
23
|
+
end
|
18
24
|
end
|
19
25
|
|
20
26
|
def process_before
|
21
|
-
fail PhTools::Error, "#{@
|
22
|
-
fail PhTools::Error, "#{@
|
27
|
+
fail PhTools::Error, "#{@target_folder} does not exist" unless File.exist?(@target_folder)
|
28
|
+
fail PhTools::Error, "#{@target_folder} is not a directory" unless File.directory?(@target_folder)
|
23
29
|
begin
|
24
30
|
Dir.mkdir @raw_folder unless Dir.exist?(@raw_folder)
|
25
31
|
Dir.mkdir @video_folder unless Dir.exist?(@video_folder)
|
26
32
|
rescue
|
27
|
-
raise PhTools::Error, "Unable to make dir inside '#{@
|
33
|
+
raise PhTools::Error, "Unable to make dir inside '#{@target_folder}'"
|
28
34
|
end
|
29
35
|
end
|
30
36
|
|
31
|
-
def process_file(
|
32
|
-
|
33
|
-
file_type =
|
37
|
+
def process_file(phfile)
|
38
|
+
phfile_out = phfile.clone
|
39
|
+
file_type = phfile.extname.slice(1..-1).downcase
|
34
40
|
case
|
35
41
|
when FILE_TYPE_IMAGE_NORMAL.include?(file_type)
|
36
|
-
|
42
|
+
phfile_out.dirname = @target_folder
|
37
43
|
when FILE_TYPE_IMAGE_RAW.include?(file_type)
|
38
|
-
|
44
|
+
phfile_out.dirname = @raw_folder
|
39
45
|
when FILE_TYPE_VIDEO.include?(file_type)
|
40
|
-
|
46
|
+
phfile_out.dirname = @video_folder
|
41
47
|
when FILE_TYPE_AUDIO.include?(file_type)
|
42
|
-
|
48
|
+
phfile_out.dirname = @target_folder
|
43
49
|
end
|
44
50
|
|
45
|
-
FileUtils.mv(
|
46
|
-
|
51
|
+
FileUtils.mv(phfile.filename, phfile_out.filename) unless phfile == phfile_out
|
52
|
+
phfile_out
|
47
53
|
rescue SystemCallError => e
|
48
54
|
raise PhTools::Error, 'file moving - ' + e.message
|
49
55
|
end
|
data/lib/phrename.rb
CHANGED
@@ -3,11 +3,43 @@
|
|
3
3
|
# (c) ANB Andrew Bizyaev
|
4
4
|
|
5
5
|
require 'phtools/runner'
|
6
|
+
require 'phtools/mini_exiftool-2.3.0anb'
|
6
7
|
|
7
8
|
module PhTools
|
8
9
|
class Phrename < Runner
|
9
10
|
def self.about
|
10
|
-
"
|
11
|
+
"renames input files to phtools standard"
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def validate_options
|
17
|
+
@author = @options_cli['--author'].upcase || ''
|
18
|
+
ok, msg = PhFile.validate_author(@author)
|
19
|
+
fail PhTools::Error, msg unless ok
|
20
|
+
@user_tag_date = @options_cli['--tag_date'] || ''
|
21
|
+
end
|
22
|
+
|
23
|
+
def process_file(phfile)
|
24
|
+
phfile_out = phfile.clone
|
25
|
+
begin
|
26
|
+
tag = MiniExiftool.new(phfile.filename, timestamps: DateTime)
|
27
|
+
rescue
|
28
|
+
raise PhTools::Error, 'EXIF tags reading'
|
29
|
+
end
|
30
|
+
if @user_tag_date.empty?
|
31
|
+
dto = tag.date_time_original || tag.create_date || PhFile::ZERO_DATE
|
32
|
+
else
|
33
|
+
fail PhTools::Error, "tag #{@user_tag_date} is not found" unless tag[@user_tag_date]
|
34
|
+
fail PhTools::Error, "tag #{@user_tag_date} is not a DateTime type" unless tag[@user_tag_date].kind_of?(DateTime)
|
35
|
+
dto = tag[@user_tag_date] || PhFile::ZERO_DATE
|
36
|
+
end
|
37
|
+
phfile_out.standardize!(date_time: dto, author: @author)
|
38
|
+
FileUtils.mv(phfile.filename, phfile_out.filename) unless
|
39
|
+
phfile == phfile_out
|
40
|
+
phfile_out
|
41
|
+
rescue => e
|
42
|
+
raise PhTools::Error, 'file renaming - ' + e.message
|
11
43
|
end
|
12
44
|
end
|
13
45
|
end
|
@@ -0,0 +1,703 @@
|
|
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
|
data/lib/phtools/ph_file.rb
CHANGED
data/lib/phtools/runner.rb
CHANGED
@@ -51,8 +51,8 @@ module PhTools
|
|
51
51
|
ARGF.each_line do |line|
|
52
52
|
filename = line.chomp
|
53
53
|
begin
|
54
|
-
|
55
|
-
ftfile =
|
54
|
+
PhFile.validate_file!(filename, @file_type)
|
55
|
+
ftfile = PhFile.new(filename)
|
56
56
|
@os.output process_file(ftfile)
|
57
57
|
rescue PhTools::Error => e
|
58
58
|
PhTools.puts_error "ERROR: '#{filename}' - #{e.message}", e
|
data/lib/phtools/version.rb
CHANGED
data/lib/phtools.rb
CHANGED
@@ -27,7 +27,7 @@ Please run phtools in a terminal via CLI commands:
|
|
27
27
|
phfixfmd\t(#{Phfixfmd::about}),
|
28
28
|
phls\t(#{Phls::about}),
|
29
29
|
phmtags\t(#{Phmtags::about}),
|
30
|
-
phrename
|
30
|
+
phrename\t(#{Phrename::about}),
|
31
31
|
phtagset\t(#{Phtagset::about}).
|
32
32
|
For more information run these commands with -h option.
|
33
33
|
General info about phtools usage see at https://github.com/AndrewBiz/phtools.git
|
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.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Bizyaev
|
@@ -241,6 +241,7 @@ email:
|
|
241
241
|
executables:
|
242
242
|
- phls
|
243
243
|
- phmove
|
244
|
+
- phrename
|
244
245
|
- phtools
|
245
246
|
extensions: []
|
246
247
|
extra_rdoc_files: []
|
@@ -260,6 +261,7 @@ files:
|
|
260
261
|
- bin/stmux
|
261
262
|
- exe/phls
|
262
263
|
- exe/phmove
|
264
|
+
- exe/phrename
|
263
265
|
- exe/phtools
|
264
266
|
- lib/phbackup.rb
|
265
267
|
- lib/phclname.rb
|
@@ -273,6 +275,7 @@ files:
|
|
273
275
|
- lib/phtagset.rb
|
274
276
|
- lib/phtools.rb
|
275
277
|
- lib/phtools/error.rb
|
278
|
+
- lib/phtools/mini_exiftool-2.3.0anb.rb
|
276
279
|
- lib/phtools/ph_file.rb
|
277
280
|
- lib/phtools/runner.rb
|
278
281
|
- lib/phtools/utils/os.rb
|