halation 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/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
|