galleruby 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/README.md +124 -0
- data/Rakefile +2 -0
- data/bin/gallerubify +163 -0
- data/bin/make_titles +71 -0
- data/config.yml.dist +4 -0
- data/galleruby.gemspec +25 -0
- data/lib/galleruby.rb +6 -0
- data/lib/galleruby/album.rb +246 -0
- data/lib/galleruby/template.rb +108 -0
- data/lib/galleruby/utilities.rb +44 -0
- data/lib/galleruby/version.rb +3 -0
- data/static/close.png +0 -0
- data/static/galleruby.css +73 -0
- data/static/galleruby.js +103 -0
- data/static/jquery-1.5.min.js +16 -0
- data/static/next.png +0 -0
- data/static/previous.png +0 -0
- data/templates/album.haml +62 -0
- data/templates/album.per_date.haml +6 -0
- data/templates/album.per_date.per_image.haml +4 -0
- data/templates/footer.haml +10 -0
- data/templates/header.haml +10 -0
- data/templates/index.haml +18 -0
- data/templates/index.per_year.haml +5 -0
- data/templates/index.per_year.per_album.haml +4 -0
- metadata +105 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
Galleruby
|
2
|
+
=========
|
3
|
+
|
4
|
+
Galleruby is a simple Ruby script to automatically generate a static HTML
|
5
|
+
gallery (series of different albums) for your local photo collection. It's
|
6
|
+
written to publish my personal photos on Amazon S3. I just run this script then
|
7
|
+
s3sync.rb the resulting output to my S3 bucket.
|
8
|
+
|
9
|
+
You can see an example setup of Galleruby running on Amazon S3 here:
|
10
|
+
|
11
|
+
[http://galleruby.devsoft.no](http://galleruby.devsoft.no)
|
12
|
+
|
13
|
+
It's not very configurable, and it makes some assumptions that might not be true
|
14
|
+
for your picture setup. I'm aware of the following ones:
|
15
|
+
|
16
|
+
* All photos need the EXIF DateTime tag set.
|
17
|
+
* Files need to have jpg or jpeg as their extension (case insensitive).
|
18
|
+
* Your albums are sorted into directories in a common source directory, and albums do not have sub-directories.
|
19
|
+
|
20
|
+
If you remove any of these limitations, or find others, please let me know! :-)
|
21
|
+
|
22
|
+
As an example of the layout, this is what
|
23
|
+
[http://galleruby.devsoft.no](http://galleruby.devsoft.no) has locally:
|
24
|
+
~/Pictures/Albums/
|
25
|
+
Hiking at Daley Ranch/
|
26
|
+
IMG_0832.JPG
|
27
|
+
IMG_0855.JPG
|
28
|
+
IMG_0864.JPG
|
29
|
+
IMG_0868.JPG
|
30
|
+
IMG_0877.JPG
|
31
|
+
IMG_0890.JPG
|
32
|
+
Joshua Tree Climbing/
|
33
|
+
IMG_4420.JPG
|
34
|
+
IMG_4425.JPG
|
35
|
+
IMG_4428.JPG
|
36
|
+
IMG_4429.JPG
|
37
|
+
IMG_4437.JPG
|
38
|
+
IMG_4450.JPG
|
39
|
+
IMG_4455.JPG
|
40
|
+
IMG_4458.JPG
|
41
|
+
IMG_4460.JPG
|
42
|
+
IMG_4461.JPG
|
43
|
+
IMG_4467.JPG
|
44
|
+
|
45
|
+
Galleruby isn't very user-friendly, but it gets the job done for me - and maybe
|
46
|
+
it'll get the job done for you too! (or maybe some day grow into something more
|
47
|
+
general, if I get some user feedback)
|
48
|
+
|
49
|
+
Dependencies
|
50
|
+
============
|
51
|
+
|
52
|
+
Galleruby has two external gem dependencies:
|
53
|
+
|
54
|
+
* RMagick
|
55
|
+
* HAML
|
56
|
+
|
57
|
+
You can install these using:
|
58
|
+
gem install rmagick haml
|
59
|
+
|
60
|
+
Using Galleruby
|
61
|
+
===============
|
62
|
+
|
63
|
+
First, Galleruby identifies albums eligible for upload by looking for a
|
64
|
+
.galleruby.yml file in the album directory. This file is expected to initially
|
65
|
+
contain the user-displayed title of the album (e.g. "Birthday party!") and the
|
66
|
+
shortname of the album, which is what the server-side output directory will be
|
67
|
+
called (e.g. "birthdayparty").
|
68
|
+
|
69
|
+
So, to get started, you need to generate these files using the make_titles.rb
|
70
|
+
script. It will suggest defaults you can use by pressing enter - and if you
|
71
|
+
don't want a directory included, press ctrl+D and it'll forever skip this
|
72
|
+
directory when you run make_titles.rb (and not create a .galleruby.yml). To
|
73
|
+
revert this behavior for a directory, delete the .galleruby.skip file.
|
74
|
+
./make_titles.rb ~/Pictures/Albums
|
75
|
+
|
76
|
+
Second, you just need to run gallerubify - but copy config.yml.dist to
|
77
|
+
config.yml and edit it first. Running gallerubify will take some time, as it's
|
78
|
+
generating three resized versions of your files for publishing. You can change
|
79
|
+
what these sizes are by editing config.yml.
|
80
|
+
./gallerubify.rb ~/Pictures/Albums
|
81
|
+
|
82
|
+
Third, you need to put the static directory in your output dir:
|
83
|
+
cp -r static output/
|
84
|
+
|
85
|
+
Example
|
86
|
+
=======
|
87
|
+
|
88
|
+
Here's how you'd get started, assuming the above layout. Notice that defaults
|
89
|
+
were accepted for most values except the title of the birthday party album, and
|
90
|
+
that we skipped publishing "Very private photos" by pressing ctrl-D.
|
91
|
+
|
92
|
+
$ ./make_titles.rb ~/Pictures/Albums
|
93
|
+
> Directory Hiking at Daley Ranch, 6 files
|
94
|
+
What should the title be? [Hiking at Daley Ranch]
|
95
|
+
|
96
|
+
What should the link name be? [hikingatdaleyranch]
|
97
|
+
|
98
|
+
> Directory Joshua Tree Climbing, 11 files
|
99
|
+
What should the title be? [Joshua Tree Climbing]
|
100
|
+
|
101
|
+
What should the link name be? [joshuatreeclimbing]
|
102
|
+
|
103
|
+
> Directory Very Private Album, 1 files
|
104
|
+
What should the title be? [Very Private Album]
|
105
|
+
^D
|
106
|
+
Skipping album
|
107
|
+
|
108
|
+
$ ./gallerubify.rb ~/Pictures/Albums
|
109
|
+
Hiking at Daley Ranch: Processing album
|
110
|
+
Hiking at Daley Ranch: Rendering HTML
|
111
|
+
Joshua Tree Climbing: Processing album
|
112
|
+
Joshua Tree Climbing: Rendering HTML
|
113
|
+
All done! Generating index.
|
114
|
+
|
115
|
+
$ cp -vr static output/
|
116
|
+
static -> output/static
|
117
|
+
static/close.png -> output/static/close.png
|
118
|
+
static/galleruby.css -> output/static/galleruby.css
|
119
|
+
static/galleruby.js -> output/static/galleruby.js
|
120
|
+
static/jquery-1.5.min.js -> output/static/jquery-1.5.min.js
|
121
|
+
static/next.png -> output/static/next.png
|
122
|
+
static/previous.png -> output/static/previous.png
|
123
|
+
|
124
|
+
$ s3sync.rb -vrp output/ my_gallery_bucket:
|
data/Rakefile
ADDED
data/bin/gallerubify
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'yaml'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
require 'galleruby'
|
8
|
+
|
9
|
+
def main
|
10
|
+
# These are the default options
|
11
|
+
default_config = {
|
12
|
+
:title => 'My Gallery',
|
13
|
+
:thumb => [320, 256],
|
14
|
+
:medium => [800, 600],
|
15
|
+
:large => [1280, 1024],
|
16
|
+
:templates => "#{File.dirname(__FILE__)}/../templates",
|
17
|
+
:static => "#{File.dirname(__FILE__)}/../static",
|
18
|
+
:output => 'output',
|
19
|
+
:verbose => false,
|
20
|
+
:force => false
|
21
|
+
}
|
22
|
+
|
23
|
+
config_file = nil
|
24
|
+
config = {}
|
25
|
+
parser = OptionParser.new do |opts|
|
26
|
+
opts.banner = "Usage: gallerubify [OPTION] ... DIR"
|
27
|
+
opts.program_name = 'gallerubify'
|
28
|
+
opts.version = Galleruby::VERSION
|
29
|
+
|
30
|
+
opts.on("-c", "--config FILE", "Read configuration options from FILE.") do |c|
|
31
|
+
config_file = c
|
32
|
+
end
|
33
|
+
|
34
|
+
opts.on("-o", "--output DIRECTORY", "Generates output gallery in DIRECTORY instead of the default 'output'") do |d|
|
35
|
+
config[:output] = d
|
36
|
+
end
|
37
|
+
|
38
|
+
opts.on("-t", "--title TITLE", "Set gallery title to TITLE") do |t|
|
39
|
+
config[:title] = t
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("--templates DIRECTORY", "Read templates from DIRECTORY.") do |d|
|
43
|
+
config[:templates] = d
|
44
|
+
end
|
45
|
+
|
46
|
+
opts.on("-s", "--static DIRECTORY", "Read static files from DIRECTORY.") do |d|
|
47
|
+
config[:static] = d
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("-f", "--[no-]force", "Force regeneration of HTML.") do |f|
|
51
|
+
config[:force] = f
|
52
|
+
end
|
53
|
+
|
54
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
55
|
+
config[:verbose] = v
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
59
|
+
puts opts
|
60
|
+
return 0
|
61
|
+
end
|
62
|
+
|
63
|
+
opts.on_tail("--version", "Show version") do
|
64
|
+
puts opts.ver
|
65
|
+
return 0
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
begin
|
70
|
+
parser.parse!
|
71
|
+
rescue OptionParser::ParseError
|
72
|
+
parser.warn $!
|
73
|
+
return 1
|
74
|
+
end
|
75
|
+
|
76
|
+
if ARGV.empty? then
|
77
|
+
puts parser
|
78
|
+
return 1
|
79
|
+
end
|
80
|
+
|
81
|
+
directory = ARGV[0]
|
82
|
+
|
83
|
+
if not config_file.nil? then
|
84
|
+
loaded_config = YAML.load(File.read(config_file)) || {}
|
85
|
+
loaded_config.keys.each do |key|
|
86
|
+
loaded_config[key.to_sym] = loaded_config[key]
|
87
|
+
loaded_config.delete(key)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
loaded_config = {}
|
91
|
+
end
|
92
|
+
|
93
|
+
# This sets the right order of precedence of default config, config file and
|
94
|
+
# command-line options. Commandline options are the most important, then
|
95
|
+
# loaded options, and at last it falls back to the default config.
|
96
|
+
config = default_config.merge(loaded_config.merge(config))
|
97
|
+
|
98
|
+
# We make sure to copy all the static resources.
|
99
|
+
puts "Copying static resources to output."
|
100
|
+
Dir["#{config[:static]}/*"].each do |path|
|
101
|
+
output_path = "#{config[:output]}/static/#{File.basename(path)}"
|
102
|
+
FileUtils.cp(path, output_path)
|
103
|
+
puts "cp #{path} #{output_path}" if config[:verbose]
|
104
|
+
end
|
105
|
+
|
106
|
+
puts "Done! Enumerating albums."
|
107
|
+
|
108
|
+
# This dynamically generates a list of all the HAML templates referenced during
|
109
|
+
# a render of album.haml, which we use to figure out if any of them have been
|
110
|
+
# modified since last we generated.
|
111
|
+
deps = Galleruby::TemplateDependencyCalculator.new('album', config)
|
112
|
+
templates_modified = deps.files.collect { |file|
|
113
|
+
path = deps.template_path(file)
|
114
|
+
File.exist?(path) ? File.mtime(path) : Time.now
|
115
|
+
}
|
116
|
+
templates_modified = templates_modified.max
|
117
|
+
|
118
|
+
encountered_links = {}
|
119
|
+
|
120
|
+
# We iterate over each directory inside the directory passed on the commandline,
|
121
|
+
# checking if any of them are considered valid albums (have .galleruby.yml etc,
|
122
|
+
# see Album#valid?) and regenerate thumbnails & HTML if its needed.
|
123
|
+
albums_by_year = Hash.new { |hash, key| hash[key] = [] }
|
124
|
+
Dir.new(directory).each do |album|
|
125
|
+
album = Galleruby::Album.new(directory, album)
|
126
|
+
|
127
|
+
next if not album.valid?
|
128
|
+
|
129
|
+
if encountered_links.has_key?(album.link) then
|
130
|
+
puts "#{album.name}: WARNING! This album has the same link name as '#{encountered_links[album.link]}', skipping."
|
131
|
+
next
|
132
|
+
end
|
133
|
+
|
134
|
+
encountered_links[album.link] = album.name
|
135
|
+
|
136
|
+
if config[:force] or album.needs_updating?(config[:output], templates_modified) then
|
137
|
+
puts "#{album.name}: Processing album"
|
138
|
+
if not album.process(config, config[:output]) then
|
139
|
+
puts "#{album.name}: WARNING! No images to process, skipping"
|
140
|
+
next
|
141
|
+
end
|
142
|
+
|
143
|
+
puts "#{album.name}: Rendering HTML"
|
144
|
+
album.render_to(config, config[:output])
|
145
|
+
else
|
146
|
+
puts "#{album.name}: No update needed, skipping"
|
147
|
+
end
|
148
|
+
|
149
|
+
albums_by_year[album.year] << album
|
150
|
+
end
|
151
|
+
|
152
|
+
puts "All done! Generating index."
|
153
|
+
|
154
|
+
# Finally we generate the index unconditionally, since it's a really cheap
|
155
|
+
# operation. It's possible that we should not do this unless neeed, so that
|
156
|
+
# index.html's mtime will have some value.
|
157
|
+
albums_by_year = albums_by_year.sort_by { |e| e[0] }.reverse.map {|year, albums| {:year => year, :albums => albums.map {|album| album.template_info }.sort_by {|album| album[:first]}.reverse } }
|
158
|
+
Galleruby::Template.new('index', config).render_to("#{config[:output]}/index.html", {:albums_by_year => albums_by_year}, config[:output])
|
159
|
+
|
160
|
+
return 0
|
161
|
+
end
|
162
|
+
|
163
|
+
exit main
|
data/bin/make_titles
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'ftools'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'yaml'
|
6
|
+
|
7
|
+
if ARGV.empty? then
|
8
|
+
puts "Syntax: #{$0} <directory>"
|
9
|
+
exit(1);
|
10
|
+
end
|
11
|
+
|
12
|
+
directory = ARGV[0]
|
13
|
+
Dir.new(directory).each { |album|
|
14
|
+
settings_file = "#{directory}/#{album}/.galleruby.yml"
|
15
|
+
skip_file = "#{directory}/#{album}/.galleruby.skip"
|
16
|
+
|
17
|
+
next if album.start_with? '.'
|
18
|
+
next if not File.directory? "#{directory}/#{album}"
|
19
|
+
next if File.exist? skip_file
|
20
|
+
|
21
|
+
info = {}
|
22
|
+
if File.exist? settings_file then
|
23
|
+
info = YAML::load(File.read(settings_file))
|
24
|
+
end
|
25
|
+
|
26
|
+
next if info.has_key? 'link' and info.has_key? 'title'
|
27
|
+
|
28
|
+
puts "> Directory #{album}, #{Dir.entries(directory + "/" + album).length - 2} files"
|
29
|
+
|
30
|
+
if not info.has_key? 'title' then
|
31
|
+
default_title = album.sub(/^\d+-\d+-\d+( - \d+)?/, '').strip
|
32
|
+
puts " What should the title be? [#{default_title}]"
|
33
|
+
title = STDIN.gets
|
34
|
+
if title.nil?
|
35
|
+
FileUtils.touch skip_file
|
36
|
+
puts " Skipping album"
|
37
|
+
next
|
38
|
+
else
|
39
|
+
title = title.chomp
|
40
|
+
end
|
41
|
+
|
42
|
+
if title.empty? then
|
43
|
+
title = default_title
|
44
|
+
end
|
45
|
+
|
46
|
+
info['title'] = title
|
47
|
+
end
|
48
|
+
|
49
|
+
if not info.has_key? 'link' then
|
50
|
+
default_link = info['title'].sub(/^\d+-\d+-\d+( - \d+)?/, '').downcase
|
51
|
+
default_link = default_link.sub('ø', 'oe').sub('å', 'aa').sub('æ', 'ae')
|
52
|
+
default_link.gsub!(/[^a-z0-9_-]/, '')
|
53
|
+
puts " What should the link name be? [#{default_link}]"
|
54
|
+
link = STDIN.gets
|
55
|
+
if link.nil?
|
56
|
+
FileUtils.touch skip_file
|
57
|
+
puts " Skipping album"
|
58
|
+
next
|
59
|
+
else
|
60
|
+
link = link.chomp
|
61
|
+
end
|
62
|
+
|
63
|
+
if link.empty? then
|
64
|
+
link = default_link
|
65
|
+
end
|
66
|
+
|
67
|
+
info['link'] = link
|
68
|
+
end
|
69
|
+
|
70
|
+
File.open(settings_file, 'w') {|file| file.write(YAML.dump(info)) }
|
71
|
+
}
|
data/config.yml.dist
ADDED
data/galleruby.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "galleruby/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "galleruby"
|
7
|
+
s.version = Galleruby::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Jørgen P. Tjernø"]
|
10
|
+
s.email = ["jorgenpt@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/jorgenpt/galleruby"
|
12
|
+
s.summary = %q{A tool to automatically generate a static HTML gallery}
|
13
|
+
s.description = %q{Galleruby allows you to automatically generate a static HTML gallery from a set of directories containing photos - each directory an album.
|
14
|
+
It is indended to allow you to publish this on static file hosts like Amazon S3.}
|
15
|
+
|
16
|
+
s.rubyforge_project = "galleruby"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
|
23
|
+
s.add_dependency 'haml'
|
24
|
+
s.add_dependency 'rmagick'
|
25
|
+
end
|
data/lib/galleruby.rb
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'yaml'
|
3
|
+
require 'rmagick'
|
4
|
+
|
5
|
+
module Galleruby
|
6
|
+
TRACK_ALLOCATIONS = false
|
7
|
+
EXIF_DATE_FORMAT = '%Y:%m:%d %H:%M:%S'
|
8
|
+
|
9
|
+
class Album
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Album representing the passed in name in the passed in directory.
|
13
|
+
def initialize(directory, name)
|
14
|
+
@name = name
|
15
|
+
@path = "#{directory}/#{name}"
|
16
|
+
@settings_file = "#{@path}/.galleruby.yml"
|
17
|
+
@skip_file = "#{@path}/.galleruby.skip"
|
18
|
+
|
19
|
+
|
20
|
+
skiplist_file = "#{@skip_file}list"
|
21
|
+
if File.exist? skiplist_file then
|
22
|
+
@skiplist = YAML::load(File.read(skiplist_file))
|
23
|
+
end
|
24
|
+
@skiplist ||= []
|
25
|
+
|
26
|
+
if valid? then
|
27
|
+
@info = YAML::load(File.read(@settings_file))
|
28
|
+
# YAML serializes DateTime as Time, so we convert back.
|
29
|
+
@info['first'] = @info['first'].to_datetime if @info.has_key?('first')
|
30
|
+
end
|
31
|
+
|
32
|
+
@info ||= {}
|
33
|
+
|
34
|
+
@images_by_date = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# Whether or not the input directory is considered a valid Galleruby album,
|
38
|
+
# i.e. if it has the metadata-file, is not a 'hidden' directory and does not
|
39
|
+
# have a blacklist (.galleruby.skip) file.
|
40
|
+
def valid?
|
41
|
+
return false if @name.start_with? '.'
|
42
|
+
return false if not File.directory? @path
|
43
|
+
return false if not File.exist? @settings_file
|
44
|
+
return false if File.exist? @skip_file
|
45
|
+
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
|
49
|
+
# When the output HTML was last generated.
|
50
|
+
def last_updated output_directory
|
51
|
+
output_file = "#{output_directory}/#{@info['link']}/index.html"
|
52
|
+
if File.exist? output_file then
|
53
|
+
File.mtime output_file
|
54
|
+
else
|
55
|
+
Time.at(0)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Whether or not the album needs to update the generated HTML file and
|
60
|
+
# possibly the resized images, based on when the input images were modified
|
61
|
+
# and when the input HAML templates were last modified.
|
62
|
+
def needs_updating?(output_directory, templates_modified)
|
63
|
+
updated = last_updated output_directory
|
64
|
+
|
65
|
+
# If the template is more recent, we need to update.
|
66
|
+
return true if templates_modified > updated
|
67
|
+
|
68
|
+
# If any of the images are more recent, we need to update.
|
69
|
+
Dir.new(@path).each do |entry|
|
70
|
+
next if not entry.match /\.jpe?g$/i
|
71
|
+
return true if updated < File.mtime("#{@path}/#{entry}")
|
72
|
+
end
|
73
|
+
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
|
77
|
+
# Whether or not the passed in filename needs to be generated from its
|
78
|
+
# input, given when the input was last updated.
|
79
|
+
def file_needs_updating?(output_filename, original_mtime)
|
80
|
+
return true if not File.exist? output_filename
|
81
|
+
return true if File.mtime(output_filename) < original_mtime
|
82
|
+
|
83
|
+
# TODO: Check for 'first' etc in @info.
|
84
|
+
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
|
88
|
+
# Process generates any resized images for the album as needed, and also
|
89
|
+
# generates metadata about the album that's cached in .galleruby.yml inside
|
90
|
+
# the albums source directory
|
91
|
+
def process(config, output_directory)
|
92
|
+
to_process = []
|
93
|
+
Dir.new(@path).each { |entry|
|
94
|
+
next if not entry.match /\.jpe?g$/i
|
95
|
+
next if @skiplist.include? entry
|
96
|
+
|
97
|
+
to_process << entry
|
98
|
+
}
|
99
|
+
|
100
|
+
return false if to_process.empty?
|
101
|
+
|
102
|
+
output_album = "#{output_directory}/#{@info['link']}"
|
103
|
+
output_thumb = "#{output_album}/small"
|
104
|
+
output_medium = "#{output_album}/medium"
|
105
|
+
output_large = "#{output_album}/large"
|
106
|
+
|
107
|
+
FileUtils.mkdir_p [output_thumb, output_medium, output_large]
|
108
|
+
|
109
|
+
@images_by_date = Hash.new {|hash, key| hash[key] = [] }
|
110
|
+
first_taken, last_taken = nil, nil
|
111
|
+
|
112
|
+
# We go over each (loosely defined) valid image in the directory, and
|
113
|
+
# generate any thumbnail, medium or large versions needed. In addition, we
|
114
|
+
# find the range of the EXIF DateTime header for the album, so that we
|
115
|
+
# can store that as metadata for the album.
|
116
|
+
to_process.each do |entry|
|
117
|
+
filename = "#{@path}/#{entry}"
|
118
|
+
thumb_filename = "#{output_thumb}/#{entry}"
|
119
|
+
medium_filename = "#{output_medium}/#{entry}"
|
120
|
+
large_filename = "#{output_large}/#{entry}"
|
121
|
+
|
122
|
+
image = LazyObject.new { o = Magick::Image.read(filename).first; o.auto_orient!; o }
|
123
|
+
original_mtime = File.mtime filename
|
124
|
+
|
125
|
+
if file_needs_updating?(large_filename, original_mtime) then
|
126
|
+
new_image = image.resize_to_fit(*config[:large])
|
127
|
+
new_image.write(large_filename)
|
128
|
+
image.destroy!
|
129
|
+
|
130
|
+
image = new_image
|
131
|
+
end
|
132
|
+
|
133
|
+
if file_needs_updating?(medium_filename, original_mtime) then
|
134
|
+
medium_image = image.resize_to_fit(*config[:medium])
|
135
|
+
medium_image.write(medium_filename)
|
136
|
+
medium_image.destroy!
|
137
|
+
end
|
138
|
+
|
139
|
+
if file_needs_updating?(thumb_filename, original_mtime) then
|
140
|
+
thumb_image = image.resize_to_fit(*config[:thumb])
|
141
|
+
thumb_image.write(thumb_filename)
|
142
|
+
else
|
143
|
+
thumb_image = Magick::Image.ping(thumb_filename).first
|
144
|
+
end
|
145
|
+
|
146
|
+
taken = thumb_image.get_exif_by_entry('DateTimeOriginal').first[1]
|
147
|
+
taken = DateTime.strptime(taken, EXIF_DATE_FORMAT)
|
148
|
+
|
149
|
+
if last_taken.nil? then
|
150
|
+
last_taken = taken
|
151
|
+
else
|
152
|
+
last_taken = taken if taken > last_taken
|
153
|
+
end
|
154
|
+
|
155
|
+
if first_taken.nil? then
|
156
|
+
first_taken = taken
|
157
|
+
else
|
158
|
+
first_taken = taken if taken < first_taken
|
159
|
+
end
|
160
|
+
|
161
|
+
@images_by_date[taken.strftime('%F')] << {
|
162
|
+
:taken => taken,
|
163
|
+
:data => {
|
164
|
+
:filename => entry,
|
165
|
+
:thumb_width => thumb_image.columns,
|
166
|
+
:thumb_height => thumb_image.rows
|
167
|
+
}
|
168
|
+
}
|
169
|
+
|
170
|
+
thumb_image.destroy!
|
171
|
+
if not image.nil? and (image.is_a?(LazyObject) and image.was_initialized?) then
|
172
|
+
image.destroy!
|
173
|
+
end
|
174
|
+
|
175
|
+
if TRACK_ALLOCATIONS and num_allocated > 0 then
|
176
|
+
puts "#{name}: Num allocated: #{num_allocated}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
@info['first'] = first_taken
|
181
|
+
if first_taken.strftime('%F') == last_taken.strftime('%F') then
|
182
|
+
@info['date'] = first_taken.strftime('%e. %b, %Y').lstrip
|
183
|
+
else
|
184
|
+
range_start = first_taken.strftime('%e').lstrip
|
185
|
+
if first_taken.year == last_taken.year then
|
186
|
+
if first_taken.month != last_taken.month then
|
187
|
+
range_start << first_taken.strftime('. %b')
|
188
|
+
end
|
189
|
+
else
|
190
|
+
range_start << first_taken.strftime('. %b, %Y')
|
191
|
+
end
|
192
|
+
|
193
|
+
date_range = "#{range_start} - #{last_taken.strftime('%e. %b, %Y').lstrip}"
|
194
|
+
@info['date'] = date_range
|
195
|
+
end
|
196
|
+
|
197
|
+
# Here we write out the original metadata + the EXIF date range we've
|
198
|
+
# identified.
|
199
|
+
File.open(@settings_file, 'w') { |file| file.write(YAML.dump(@info)) }
|
200
|
+
|
201
|
+
return true
|
202
|
+
end
|
203
|
+
|
204
|
+
# Create a HTML-file for the album.
|
205
|
+
def render_to(config, output_directory)
|
206
|
+
images_by_date = @images_by_date.sort.map do |day, images|
|
207
|
+
{
|
208
|
+
:date => Date.strptime(day),
|
209
|
+
:images => images.sort_by {|image| image[:taken]}.map {|image| image[:data]}
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
output_file = "#{output_directory}/#{@info['link']}/index.html"
|
214
|
+
Template.new('album', config).render_to(output_file, {:title => @info['title'], :images_by_date => images_by_date}, output_directory)
|
215
|
+
end
|
216
|
+
|
217
|
+
# The year that the first photo was taken in.
|
218
|
+
def year
|
219
|
+
@info['first'].strftime('%Y')
|
220
|
+
end
|
221
|
+
|
222
|
+
def link
|
223
|
+
@info['link']
|
224
|
+
end
|
225
|
+
|
226
|
+
# Data needed for generation of the index document.
|
227
|
+
def template_info
|
228
|
+
{:name => @info['title'], :link => @info['link'], :date => @info['date'], :first => @info['first']}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# If we TRACK_ALLOCATIONS, we keep a count of "currently allocated images", so
|
233
|
+
# that we can identify memory leaks.
|
234
|
+
num_allocated = 0
|
235
|
+
if TRACK_ALLOCATIONS then
|
236
|
+
Magick.trace_proc = Proc.new do |which, description, id, method|
|
237
|
+
if which == :c then
|
238
|
+
num_allocated += 1
|
239
|
+
elsif which == :d then
|
240
|
+
num_allocated -= 1
|
241
|
+
else
|
242
|
+
puts "#{which} #{id} #{description} (from #{method})"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|