halation 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +211 -0
- data/bin/halation +4 -1
- data/doc/Halation.html +14 -4
- data/doc/Halation/AttrHelpers.html +288 -0
- data/doc/Halation/Coerce.html +373 -0
- data/doc/Halation/Config.html +732 -0
- data/doc/Halation/Config/Camera.html +628 -0
- data/doc/Halation/Config/Lens.html +540 -0
- data/doc/Halation/Engine.html +928 -0
- data/doc/Halation/ExifToolImage.html +486 -0
- data/doc/Halation/Roll.html +916 -0
- data/doc/Halation/Roll/Frame.html +842 -0
- data/doc/Halation/Script.html +506 -0
- data/doc/_index.html +112 -1
- data/doc/class_list.html +1 -1
- data/doc/file.LICENSE.html +1 -1
- data/doc/file.README.html +211 -1
- data/doc/index.html +211 -1
- data/doc/method_list.html +432 -0
- data/doc/top-level-namespace.html +1 -1
- data/lib/halation.rb +4 -0
- data/lib/halation/coerce.rb +21 -0
- data/lib/halation/config.rb +53 -0
- data/lib/halation/config/camera.rb +42 -0
- data/lib/halation/config/lens.rb +32 -0
- data/lib/halation/engine.rb +91 -0
- data/lib/halation/roll.rb +57 -0
- data/lib/halation/roll/frame.rb +47 -0
- data/lib/halation/script.rb +150 -0
- data/lib/halation/tools/exif_tool/image.rb +33 -0
- data/lib/halation/version.rb +1 -1
- metadata +42 -3
@@ -0,0 +1,21 @@
|
|
1
|
+
module Halation
|
2
|
+
# A collection of methods to coerce values into a desired type.
|
3
|
+
class Coerce
|
4
|
+
|
5
|
+
# @return [String, nil]
|
6
|
+
def self.string(value)
|
7
|
+
value &&= value.to_s
|
8
|
+
end
|
9
|
+
|
10
|
+
# @return [Integer, nil]
|
11
|
+
def self.integer(value)
|
12
|
+
value &&= value.to_i
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Boolean]
|
16
|
+
def self.boolean(value)
|
17
|
+
!!value
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'coerce'
|
3
|
+
require_relative 'config/camera'
|
4
|
+
|
5
|
+
module Halation
|
6
|
+
# Application-wide configuraton.
|
7
|
+
class Config
|
8
|
+
# :nodoc:
|
9
|
+
DEFAULT_CONFIG_PATH = File.expand_path("~/.halation/config.yml").freeze
|
10
|
+
|
11
|
+
attr_reader :artist
|
12
|
+
attr_reader :copyright
|
13
|
+
attr_reader :cameras
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
reset
|
17
|
+
end
|
18
|
+
|
19
|
+
# Reset the configuration to default values.
|
20
|
+
def reset
|
21
|
+
@artist = nil
|
22
|
+
@copyright = nil
|
23
|
+
@cameras = []
|
24
|
+
end
|
25
|
+
|
26
|
+
# Load the configuration from a YAML file.
|
27
|
+
def load_file(file_path = nil)
|
28
|
+
file_path ||= File.expand_path("./config.yml") if File.exists?("./config.yml")
|
29
|
+
file_path ||= DEFAULT_CONFIG_PATH
|
30
|
+
|
31
|
+
reset
|
32
|
+
|
33
|
+
YAML.load_file(file_path).tap do |config|
|
34
|
+
@artist = Coerce.string(config["artist"])
|
35
|
+
@copyright = Coerce.string(config["copyright"])
|
36
|
+
|
37
|
+
(config["cameras"] || []).each do |camera|
|
38
|
+
@cameras << Camera.new(camera)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [String] list of configuration settings.
|
44
|
+
def to_s
|
45
|
+
[
|
46
|
+
"Artist: #{@artist}",
|
47
|
+
"Copyright: #{@copyright}",
|
48
|
+
@cameras.map(&:to_s)
|
49
|
+
].join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../coerce'
|
2
|
+
require_relative 'lens'
|
3
|
+
|
4
|
+
module Halation
|
5
|
+
class Config
|
6
|
+
# A camera profile.
|
7
|
+
class Camera
|
8
|
+
# A user-created ID.
|
9
|
+
attr_reader :tag
|
10
|
+
attr_reader :make
|
11
|
+
attr_reader :model
|
12
|
+
attr_reader :lenses
|
13
|
+
|
14
|
+
def initialize(yaml)
|
15
|
+
@tag = Coerce.string(yaml["tag"])
|
16
|
+
@make = Coerce.string(yaml["make"])
|
17
|
+
@model = Coerce.string(yaml["model"])
|
18
|
+
@lenses = []
|
19
|
+
|
20
|
+
yaml["lenses"].each do |lens|
|
21
|
+
@lenses << Lens.new(lens)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
def to_s
|
27
|
+
"Camera\n" <<
|
28
|
+
[
|
29
|
+
"Tag: #{tag}",
|
30
|
+
"Make: #{make}",
|
31
|
+
"Model: #{model}",
|
32
|
+
@lenses.map(&:to_s).join("\n")
|
33
|
+
]
|
34
|
+
.join("\n")
|
35
|
+
.lines
|
36
|
+
.map { |line| " #{line}" }
|
37
|
+
.join
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative '../coerce'
|
2
|
+
|
3
|
+
module Halation
|
4
|
+
class Config
|
5
|
+
# A lens profile.
|
6
|
+
class Lens
|
7
|
+
# A user-created ID.
|
8
|
+
attr_reader :tag
|
9
|
+
attr_reader :model
|
10
|
+
attr_reader :focal_length
|
11
|
+
|
12
|
+
def initialize(yaml)
|
13
|
+
@tag = Coerce.string(yaml["tag"])
|
14
|
+
@model = Coerce.string(yaml["model"])
|
15
|
+
@focal_length = Coerce.integer(yaml["focal_length"])
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
def to_s
|
20
|
+
"Lens\n" <<
|
21
|
+
[
|
22
|
+
"Tag: #{tag}",
|
23
|
+
"Model: #{model}",
|
24
|
+
"Focal Length: #{focal_length}",
|
25
|
+
]
|
26
|
+
.map { |line| " #{line}" }
|
27
|
+
.join("\n")
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require_relative 'config'
|
2
|
+
require_relative 'roll'
|
3
|
+
require_relative 'tools/exif_tool/image'
|
4
|
+
|
5
|
+
module Halation
|
6
|
+
# Performs modifications to the image Exif data.
|
7
|
+
class Engine
|
8
|
+
# Array of image extensions to search for.
|
9
|
+
attr_accessor :image_extensions
|
10
|
+
# Suppress output to stdout if true.
|
11
|
+
attr_accessor :silent
|
12
|
+
|
13
|
+
attr_reader :config
|
14
|
+
attr_reader :roll
|
15
|
+
|
16
|
+
# @see #run
|
17
|
+
def self.run(opts = {})
|
18
|
+
new(opts).run
|
19
|
+
end
|
20
|
+
|
21
|
+
# @option opts [String] :config_path (nil)
|
22
|
+
# Override the config file that should be used.
|
23
|
+
# @option opts [String] :working_dir (".")
|
24
|
+
# Override the working directory (contains images and roll.yml).
|
25
|
+
# @option opts [Array<String>] :image_extensions
|
26
|
+
# Override the list of image extensions to search for.
|
27
|
+
# @option opts [Boolean] :silent
|
28
|
+
# Suppress output to stdout.
|
29
|
+
def initialize(opts = {})
|
30
|
+
config_path = opts[:config_path]
|
31
|
+
@silent = !!opts[:silent]
|
32
|
+
@working_dir = File.expand_path(opts[:working_dir] || ".")
|
33
|
+
@image_extensions = opts[:image_extensions] || ["tif", "tiff", "jpg", "jpeg"]
|
34
|
+
|
35
|
+
@config = Config.new
|
36
|
+
@config.load_file(config_path)
|
37
|
+
|
38
|
+
@roll = Roll.new
|
39
|
+
@roll.load_file("#{@working_dir}/roll.yml")
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Array<String>] detected image files to process, in ascending
|
43
|
+
# alphabetical order.
|
44
|
+
def image_files
|
45
|
+
Dir[*@image_extensions.map { |ext| "#{@working_dir}/*.#{ext}" }]
|
46
|
+
.sort[0...@roll.frames.count]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Process the batch of images.
|
50
|
+
def run
|
51
|
+
_image_files = image_files
|
52
|
+
|
53
|
+
specified_camera = @roll.camera
|
54
|
+
camera = @config.cameras.find { |camera| camera.tag == specified_camera }
|
55
|
+
raise "Camera #{specified_camera} not found in config.yml" unless camera
|
56
|
+
|
57
|
+
@roll.frames.each_with_index do |frame, i|
|
58
|
+
break if i >= _image_files.count
|
59
|
+
|
60
|
+
specified_lens = frame.lens || @roll.lens
|
61
|
+
lens = camera.lenses.find { |lens| lens.tag == specified_lens }
|
62
|
+
raise "Lens #{specified_lens} not found for frame #{frame.number}" unless lens
|
63
|
+
|
64
|
+
relative_path = _image_files[i].gsub(%r(^#{Dir.pwd}/), "")
|
65
|
+
puts "Processing frame #{frame.number}\n #{relative_path}" unless @silent
|
66
|
+
|
67
|
+
ExifToolImage.new(_image_files[i]).tap do |exif|
|
68
|
+
exif["Artist"] = @roll.artist || @config.artist
|
69
|
+
exif["Copyright"] = @roll.copyright || @config.copyright
|
70
|
+
|
71
|
+
date_created = Time.new(frame.date || @roll.date)
|
72
|
+
exif["DateTimeOriginal"] = date_created
|
73
|
+
exif["CreateDate"] = date_created
|
74
|
+
|
75
|
+
exif["Make"] = camera.make
|
76
|
+
exif["Model"] = camera.model
|
77
|
+
exif["LensModel"] = lens.model
|
78
|
+
exif["ISO"] = @roll.iso
|
79
|
+
exif["ExposureTime"] = frame.shutter || @roll.shutter
|
80
|
+
exif["FNumber"] = frame.aperture || @roll.aperture
|
81
|
+
exif["FocalLength"] = frame.focal_length || lens.focal_length || @roll.focal_length
|
82
|
+
exif["Flash"] = frame.flash ? 1 : 0
|
83
|
+
exif["Orientation"] = frame.orientation || 1
|
84
|
+
|
85
|
+
exif.save
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require_relative 'coerce'
|
3
|
+
require_relative 'roll/frame'
|
4
|
+
|
5
|
+
module Halation
|
6
|
+
# Settings for a roll of film.
|
7
|
+
class Roll
|
8
|
+
# Artist for the roll of film.
|
9
|
+
attr_reader :artist
|
10
|
+
# Copyright for the roll of film.
|
11
|
+
attr_reader :copyright
|
12
|
+
# Default date for all frames (optional).
|
13
|
+
attr_reader :date
|
14
|
+
# Tag of the cameara used.
|
15
|
+
attr_reader :camera
|
16
|
+
# Tag of the default lens used (optional).
|
17
|
+
attr_reader :lens
|
18
|
+
# ISO of the roll of film.
|
19
|
+
attr_reader :iso
|
20
|
+
# Array of frames on the roll of film.
|
21
|
+
attr_reader :frames
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
reset
|
25
|
+
end
|
26
|
+
|
27
|
+
# Reset the configuration to default values.
|
28
|
+
def reset
|
29
|
+
@artist = nil
|
30
|
+
@copyright = nil
|
31
|
+
@date = nil
|
32
|
+
@camera = nil
|
33
|
+
@lens = nil
|
34
|
+
@iso = nil
|
35
|
+
@frames = []
|
36
|
+
end
|
37
|
+
|
38
|
+
# Load the settings from a YAML file.
|
39
|
+
def load_file(file_path)
|
40
|
+
reset
|
41
|
+
|
42
|
+
YAML.load_file(file_path).tap do |roll|
|
43
|
+
@artist = Coerce.string(roll["artist"])
|
44
|
+
@copyright = Coerce.string(roll["copyright"])
|
45
|
+
@date = Coerce.string(roll["date"])
|
46
|
+
@camera = Coerce.string(roll["camera"])
|
47
|
+
@lens = Coerce.string(roll["lens"])
|
48
|
+
@iso = Coerce.integer(roll["iso"])
|
49
|
+
|
50
|
+
(roll["frames"] || []).each do |frame|
|
51
|
+
@frames << Frame.new(frame)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require_relative '../coerce'
|
2
|
+
|
3
|
+
module Halation
|
4
|
+
class Roll
|
5
|
+
# A frame on the roll of film.
|
6
|
+
class Frame
|
7
|
+
# Frame number.
|
8
|
+
attr_reader :number
|
9
|
+
# Date the frame was taken.
|
10
|
+
attr_reader :date
|
11
|
+
# Tag of the lens used.
|
12
|
+
attr_reader :lens
|
13
|
+
# Focal length of the lens, if not specified by the lens profile
|
14
|
+
# (a zoom lens).
|
15
|
+
attr_reader :focal_length
|
16
|
+
# @todo spec
|
17
|
+
# Shutter speed.
|
18
|
+
# @example "1/125", "0.5" (half second), "15" (seconds), "120" (2 minutes)
|
19
|
+
attr_reader :shutter
|
20
|
+
# F-number of the aperture setting.
|
21
|
+
attr_reader :aperture
|
22
|
+
# True if flash was fired.
|
23
|
+
attr_reader :flash
|
24
|
+
# 1 = Horizontal (normal)
|
25
|
+
# 2 = Mirror horizontal
|
26
|
+
# 3 = Rotate 180
|
27
|
+
# 4 = Mirror vertical
|
28
|
+
# 5 = Mirror horizontal and rotate 270 CW
|
29
|
+
# 6 = Rotate 90 CW
|
30
|
+
# 7 = Mirror horizontal and rotate 90 CW
|
31
|
+
# 8 = Rotate 270 CW
|
32
|
+
attr_reader :orientation
|
33
|
+
|
34
|
+
def initialize(yaml)
|
35
|
+
@number = Coerce.integer(yaml["number"])
|
36
|
+
@date = Coerce.string(yaml["date"])
|
37
|
+
@lens = Coerce.string(yaml["lens"])
|
38
|
+
@focal_length = Coerce.integer(yaml["focal_length"])
|
39
|
+
@shutter = Coerce.string(yaml["shutter"])
|
40
|
+
@aperture = Coerce.string(yaml["aperture"])
|
41
|
+
@flash = Coerce.boolean(yaml["flash"])
|
42
|
+
@orientation = Coerce.integer(yaml["orientation"])
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
require_relative 'engine'
|
5
|
+
|
6
|
+
module Halation
|
7
|
+
# The script that runs when the halation binary is executed.
|
8
|
+
class Script
|
9
|
+
private_class_method :new
|
10
|
+
|
11
|
+
# @option opts [Boolean] :args (ARGV)
|
12
|
+
# @option opts [Boolean] :output_stream (STDOUT)
|
13
|
+
# @option opts [Boolean] :skip_exit (false)
|
14
|
+
# Don't exit the program after calling a handler that would normally exit.
|
15
|
+
# Used for unit testing.
|
16
|
+
def self.run(opts = {})
|
17
|
+
args = opts[:args] || ARGV
|
18
|
+
output_stream = opts[:output_stream] || STDOUT
|
19
|
+
skip_exit = !!opts[:skip_exit]
|
20
|
+
run_engine = true
|
21
|
+
|
22
|
+
options = {}
|
23
|
+
|
24
|
+
OptionParser.new { |opts|
|
25
|
+
opts.banner = "Usage: halation [options]"
|
26
|
+
|
27
|
+
opts.on("-c", "--config=PATH", String, "Config file path") do |config_path|
|
28
|
+
options[:config_path] = config_path
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("--dry", "Dry run") do
|
32
|
+
options[:dry_run] = true
|
33
|
+
# TODO: Implement
|
34
|
+
raise NotImplementedError, "Dry run option is not yet implemented."
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("-h", "--help", "Print this help") do
|
38
|
+
output_stream.puts opts
|
39
|
+
run_engine = false
|
40
|
+
exit unless skip_exit
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("--new-config", "Generate a new config file") do |path|
|
44
|
+
# TODO: Implement
|
45
|
+
raise NotImplementedError, "Generate config option is not yet implemented."
|
46
|
+
run_engine = false
|
47
|
+
exit unless skip_exit
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("--new-roll", "Generate a new roll.yml file") do
|
51
|
+
generate_new_roll
|
52
|
+
run_engine = false
|
53
|
+
exit unless skip_exit
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on("-p", "--print-config", "Print the configuration settings") do
|
57
|
+
# TODO: Implement
|
58
|
+
raise NotImplementedError, "Print config option is not yet implemented."
|
59
|
+
run_engine = false
|
60
|
+
exit unless skip_exit
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on("-r", "--recursive", "Traverse into subdirectories") do
|
64
|
+
# TODO: Implement
|
65
|
+
raise NotImplementedError, "Recursive option is not yet implemented."
|
66
|
+
end
|
67
|
+
|
68
|
+
opts.on("--silent", "Suppress messages to stdout.") do
|
69
|
+
options[:silent] = true
|
70
|
+
end
|
71
|
+
|
72
|
+
opts.on("-v", "--version", "Print the version information") do
|
73
|
+
output_stream.puts "halation #{Halation::VERSION}"
|
74
|
+
run_engine = false
|
75
|
+
exit unless skip_exit
|
76
|
+
end
|
77
|
+
}.parse!(args)
|
78
|
+
|
79
|
+
Halation::Engine.run(options) if run_engine
|
80
|
+
end
|
81
|
+
|
82
|
+
# Generate a new roll.yml file.
|
83
|
+
# Copies "~/.halation/templates/roll.yml" if it exists, otherwise it uses
|
84
|
+
# a default template.
|
85
|
+
def self.generate_new_roll
|
86
|
+
roll_path = "roll.yml"
|
87
|
+
|
88
|
+
if File.exists?(roll_path)
|
89
|
+
output_stream.puts "A roll.yml file already exists in this directory."
|
90
|
+
return
|
91
|
+
end
|
92
|
+
|
93
|
+
# TODO: Make this configurable from config.yml
|
94
|
+
roll_template_path = File.expand_path("~/.halation/templates/roll.yml")
|
95
|
+
|
96
|
+
if File.exists?(roll_template_path)
|
97
|
+
FileUtils.cp(roll_template_path, ".")
|
98
|
+
else
|
99
|
+
File.open(roll_path, "w") do |f|
|
100
|
+
f.puts new_roll_content
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# @return [String] roll.yml default content
|
108
|
+
def self.new_roll_content
|
109
|
+
output = <<YAML
|
110
|
+
---
|
111
|
+
date: "2016-01-01"
|
112
|
+
camera: "rz67"
|
113
|
+
lens: 110
|
114
|
+
iso: 100
|
115
|
+
frames:
|
116
|
+
- number: 1
|
117
|
+
shutter: "1/125"
|
118
|
+
aperture: 8
|
119
|
+
- number: 2
|
120
|
+
shutter: "1/125"
|
121
|
+
aperture: 8
|
122
|
+
- number: 3
|
123
|
+
shutter: "1/125"
|
124
|
+
aperture: 8
|
125
|
+
- number: 4
|
126
|
+
shutter: "1/125"
|
127
|
+
aperture: 8
|
128
|
+
- number: 5
|
129
|
+
shutter: "1/125"
|
130
|
+
aperture: 8
|
131
|
+
- number: 6
|
132
|
+
shutter: "1/125"
|
133
|
+
aperture: 8
|
134
|
+
- number: 7
|
135
|
+
shutter: "1/125"
|
136
|
+
aperture: 8
|
137
|
+
- number: 8
|
138
|
+
shutter: "1/125"
|
139
|
+
aperture: 8
|
140
|
+
- number: 9
|
141
|
+
shutter: "1/125"
|
142
|
+
aperture: 8
|
143
|
+
- number: 10
|
144
|
+
shutter: "1/125"
|
145
|
+
aperture: 8
|
146
|
+
YAML
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|