galleruby 0.1

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.
@@ -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