galleruby 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ /output/
2
+ config.yml
3
+ *.gem
4
+ .bundle
5
+ Gemfile.lock
6
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in galleruby.gemspec
4
+ gemspec
@@ -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:
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -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
@@ -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
+ }
@@ -0,0 +1,4 @@
1
+ title: Cool Joe's Gallery
2
+ thumb: [320, 256]
3
+ medium: [800, 600]
4
+ large: [1280, 1024]
@@ -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
@@ -0,0 +1,6 @@
1
+ module Galleruby
2
+ require 'galleruby/version'
3
+ require 'galleruby/utilities'
4
+ require 'galleruby/template'
5
+ require 'galleruby/album'
6
+ end
@@ -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