montage 0.2.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.
Files changed (65) hide show
  1. data/.document +5 -0
  2. data/.gitignore +31 -0
  3. data/History.md +40 -0
  4. data/LICENSE +19 -0
  5. data/README.md +89 -0
  6. data/Rakefile +41 -0
  7. data/VERSION +1 -0
  8. data/bin/montage +22 -0
  9. data/lib/montage.rb +33 -0
  10. data/lib/montage/commands.rb +32 -0
  11. data/lib/montage/commands/generate.rb +234 -0
  12. data/lib/montage/commands/init.rb +119 -0
  13. data/lib/montage/core_ext.rb +80 -0
  14. data/lib/montage/project.rb +185 -0
  15. data/lib/montage/sass_builder.rb +37 -0
  16. data/lib/montage/source.rb +75 -0
  17. data/lib/montage/sprite.rb +132 -0
  18. data/lib/montage/templates/montage.yml +26 -0
  19. data/lib/montage/templates/sass_mixins.erb +20 -0
  20. data/lib/montage/templates/sources/book.png +0 -0
  21. data/lib/montage/templates/sources/box-label.png +0 -0
  22. data/lib/montage/templates/sources/calculator.png +0 -0
  23. data/lib/montage/templates/sources/calendar-month.png +0 -0
  24. data/lib/montage/templates/sources/camera.png +0 -0
  25. data/lib/montage/templates/sources/eraser.png +0 -0
  26. data/lib/montage/version.rb +3 -0
  27. data/montage.gemspec +145 -0
  28. data/spec/fixtures/custom_dirs/montage.yml +8 -0
  29. data/spec/fixtures/default/montage.yml +7 -0
  30. data/spec/fixtures/default/public/images/sprites/src/one.png +0 -0
  31. data/spec/fixtures/default/public/images/sprites/src/three.png +0 -0
  32. data/spec/fixtures/default/public/images/sprites/src/two.png +0 -0
  33. data/spec/fixtures/directory_config/config/montage.yml +5 -0
  34. data/spec/fixtures/missing_source/montage.yml +3 -0
  35. data/spec/fixtures/missing_source_dir/montage.yml +5 -0
  36. data/spec/fixtures/root_config/montage.yml +5 -0
  37. data/spec/fixtures/root_config/public/images/sprites/src/source_one.png +0 -0
  38. data/spec/fixtures/root_config/public/images/sprites/src/source_three.jpg +0 -0
  39. data/spec/fixtures/root_config/public/images/sprites/src/source_two +0 -0
  40. data/spec/fixtures/sources/hundred.png +0 -0
  41. data/spec/fixtures/sources/mammoth.png +0 -0
  42. data/spec/fixtures/sources/other.png +0 -0
  43. data/spec/fixtures/sources/twenty.png +0 -0
  44. data/spec/fixtures/subdirs/montage.yml +5 -0
  45. data/spec/fixtures/subdirs/sub/sub/keep +0 -0
  46. data/spec/lib/command_runner.rb +140 -0
  47. data/spec/lib/fixtures.rb +7 -0
  48. data/spec/lib/have_public_method_defined.rb +19 -0
  49. data/spec/lib/project_helper.rb +135 -0
  50. data/spec/lib/shared_project_specs.rb +32 -0
  51. data/spec/lib/shared_sprite_specs.rb +30 -0
  52. data/spec/montage/commands/generate_spec.rb +308 -0
  53. data/spec/montage/commands/init_spec.rb +120 -0
  54. data/spec/montage/core_ext_spec.rb +33 -0
  55. data/spec/montage/project_spec.rb +181 -0
  56. data/spec/montage/sass_builder_spec.rb +269 -0
  57. data/spec/montage/source_spec.rb +53 -0
  58. data/spec/montage/spec/have_public_method_defined_spec.rb +31 -0
  59. data/spec/montage/sprite_spec.rb +170 -0
  60. data/spec/rcov.opts +8 -0
  61. data/spec/spec.opts +4 -0
  62. data/spec/spec_helper.rb +19 -0
  63. data/tasks/spec.rake +17 -0
  64. data/tasks/yard.rake +11 -0
  65. metadata +249 -0
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,31 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+ Icon*
4
+
5
+ ## TEXTMATE
6
+ *.tmproj
7
+ tmtags
8
+
9
+ ## EMACS
10
+ *~
11
+ \#*
12
+ .\#*
13
+
14
+ ## VIM
15
+ *.swp
16
+
17
+ ## RUBINIUS
18
+ *.rbc
19
+ rakefile.compiled.rbc
20
+
21
+ ## PROJECT::GENERAL
22
+ .yardoc
23
+ coverage
24
+ doc
25
+ measurements
26
+ pkg
27
+ rdoc
28
+ tmp
29
+
30
+ ## PROJECT::SPECIFIC
31
+ .sass-cache
data/History.md ADDED
@@ -0,0 +1,40 @@
1
+ v0.2.0 - 2010-04-08
2
+ -------------------
3
+
4
+ * Running `montage` will now generate `_montage.sass` in the specified
5
+ config.sass directory. A separate mixin will be generated for each sprite,
6
+ with the mixin accepting three arguments: the name of the source image, an
7
+ optional horizontal offset, and an optional vertical offset. Disable the
8
+ Sass generation by setting config.sass to false.
9
+
10
+ * If pngout or pngout-darwin is available (run `which pngout pngout-darwin` to
11
+ find out), Montage will compress the generated sprites. Installing pngout
12
+ is strongly recommended; significant savings can be made on larger PNGs.
13
+
14
+ * The `montage` command accepts a '--force' option which will regenerate all
15
+ sprites even if they haven't been changed since the last run.
16
+
17
+ * Sprites will be regenerated if the file has been deleted.
18
+
19
+ v0.1.2 - 2010-04-06
20
+ -------------------
21
+
22
+ * Sprites will only be regenerated when their definition (in montage.yml) has
23
+ changed, or if the contents of the source files have changed.
24
+
25
+ * The `montage init` command now uses the highline gem to ask for the paths to
26
+ a project's source files, and the intended sprite output directory.
27
+
28
+ * Running `montage init` will copy some sample source files into the source
29
+ directory. This allows running `montage` immediately after creating the
30
+ project, to see how things work.
31
+
32
+ v0.1.1 - 2010-04-05
33
+ -------------------
34
+
35
+ * Small fix for Ruby 1.9.1, which doesn't define String#inject.
36
+
37
+ v0.1.0 - 2010-04-05
38
+ -------------------
39
+
40
+ * Initial release. Supports creation of sprites and not much else.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2009-2010 Anthony Williams
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ Montage
2
+ =======
3
+
4
+ _"[Even Rocky had a montage](http://www.youtube.com/watch?v=FIi0vFyqWAc)."_
5
+
6
+ **Source**
7
+ : [http://github.com/antw/montage](http://github.com/antw/montage)
8
+
9
+ **Author**
10
+ : Anthony Williams
11
+
12
+ **Copyright**
13
+ : 2009-2010
14
+
15
+ **License**
16
+ : MIT License
17
+
18
+ SYNOPSIS
19
+ --------
20
+
21
+ Popularised by Dave Shea in an [A List Apart](http://www.alistapart.com/articles/sprites), "sprite" images combine many smaller images into a single, larger image, with CSS then being used to divide the sprite back into it's constituent parts.
22
+
23
+ However, creating sprites burdens you with having to manually update them every time a minor change is required. Even more frustrating is the need to remember the precise background positions required when editing the CSS.
24
+
25
+ Montage is a library which, provided a simple configuration file, will automate this process, and if you're a SASS user, you're in for a treat: Montage generates mixins which make working with your sprites incredibly simple:
26
+
27
+ SASS Usage:
28
+
29
+ #navigation
30
+ a#home, a#products
31
+ +main-sprite("home")
32
+ a#products
33
+ +main-sprite-pos("products")
34
+
35
+ Generated CSS:
36
+
37
+ #navigation a#home, #navigation a#products {
38
+ background: url(/path/to/sprite.ext) 0 0 no-repeat; }
39
+
40
+ #navigation a#products {
41
+ background-position: 0 -40px; }
42
+
43
+ Montage has been split out from [Kin](http://github.com/antw/kin) -- a collection of various bits-and-bobs from my Merb projects.
44
+
45
+ Montage is pretty primitive in that it stacks each image in a single column. This is perfect when your source images are of a similar width (such as is the case with icons), but not so good when they vary significantly in size.
46
+
47
+ FEATURE LIST
48
+ ------------
49
+
50
+ Coming soon.
51
+
52
+ USAGE
53
+ -----
54
+
55
+ Coming soon.
56
+
57
+ Ward specs are run against:
58
+
59
+ * Ruby (MRI) 1.8.6 p399,
60
+ * Ruby (MRI) 1.8.7 p249,
61
+ * Ruby (YARV) 1.9.1 p378,
62
+
63
+ Montage requires RMagick which presently rules out support for JRuby and Rubinius.
64
+
65
+ CONTRIBUTING
66
+ ------------
67
+
68
+ * Fork the project, taking care not to get any in your eyes.
69
+
70
+ * Make your feature addition or bug fix.
71
+
72
+ * Add tests for it. This is especially important not only because it helps
73
+ ensure that I don't unintentionally break it in a future version, but also
74
+ since it appeases Phyllis --- the goddess of Cucumbers --- who has been
75
+ known to rain showers of fresh vegetables on those who don't write tests.
76
+
77
+ * Commit, but do not mess with the Rakefile, VERSION, or history. If you want
78
+ to have your own version, that is fine, but bump version in a commit by
79
+ itself so that I can ignore it when I pull.
80
+
81
+ * Send me a pull request. Bonus points for topic branches. But we all know
82
+ everything is made up and the points don't matter.
83
+
84
+ COPYRIGHT
85
+ ---------
86
+
87
+ Montage © 2009-2010 by [Anthony Williams](mailto:hi@antw.me). Licensed under the MIT license. Please see the {file:LICENSE} for more information.
88
+
89
+ The sample sources in lib/montage/templates/sources are courtesy of Yusuke Kamiyamane: http://p.yusukekamiyamane.com
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+
4
+ require File.expand_path('../lib/montage/version', __FILE__)
5
+
6
+ CLOBBER.include ['pkg', '*.gem', 'doc', 'coverage', 'measurements']
7
+
8
+ begin
9
+ require 'jeweler'
10
+ Jeweler::Tasks.new do |gem|
11
+ gem.name = 'montage'
12
+ gem.summary = 'Montage'
13
+ gem.homepage = 'http://github.com/antw/montage'
14
+ gem.description = 'Even Rocky had a montage.'
15
+
16
+ gem.author = 'Anthony Williams'
17
+ gem.email = 'hi@antw.me'
18
+
19
+ gem.platform = Gem::Platform::RUBY
20
+ gem.has_rdoc = false
21
+
22
+ # Dependencies.
23
+ gem.add_dependency 'activesupport', '>= 3.0.0.beta'
24
+ gem.add_dependency 'rmagick', '>= 2.12'
25
+ gem.add_dependency 'highline', '>= 1.5'
26
+
27
+ # Development dependencies.
28
+ gem.add_development_dependency 'rspec', '>= 1.3.0'
29
+ gem.add_development_dependency 'cucumber', '>= 0.6'
30
+ gem.add_development_dependency 'open4', '>= 1.0'
31
+ gem.add_development_dependency 'haml', '>= 3.0.0.beta.1'
32
+ gem.add_development_dependency 'yard', '>= 0.5'
33
+ end
34
+
35
+ Jeweler::GemcutterTasks.new
36
+ rescue LoadError
37
+ puts 'Jeweler (or a dependency) not available. Install it with: gem '\
38
+ 'install jeweler'
39
+ end
40
+
41
+ FileList['tasks/**/*.rake'].each { |task| import task }
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
data/bin/montage ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ unless $:.include?(File.dirname(__FILE__) + '/../lib')
4
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
5
+ end
6
+
7
+ require 'rubygems' unless ENV['NORUBYGEMS']
8
+
9
+ require 'montage'
10
+ require 'montage/commands'
11
+
12
+ Montage::Commands.print_masthead
13
+
14
+ case ARGV[0]
15
+ when 'init'
16
+ require 'montage/commands/init'
17
+ Montage::Commands::Init.run(ARGV)
18
+ else
19
+ require 'montage/commands/generate'
20
+ Montage::Commands::Generate.run(ARGV)
21
+ end
22
+
data/lib/montage.rb ADDED
@@ -0,0 +1,33 @@
1
+ require 'digest/sha2'
2
+ require 'erb'
3
+ require 'pathname'
4
+ require 'yaml'
5
+
6
+ # Gems.
7
+ require 'active_support/ordered_hash'
8
+ require 'rmagick'
9
+
10
+ # On with the library...
11
+ require 'montage/core_ext'
12
+ require 'montage/project'
13
+ require 'montage/sass_builder'
14
+ require 'montage/source'
15
+ require 'montage/sprite'
16
+ require 'montage/version'
17
+
18
+ module Montage
19
+ # Generic exception for all Montage exception classes.
20
+ MontageError = Class.new(StandardError)
21
+
22
+ # Raised when a project directory couldn't be found.
23
+ MissingProject = Class.new(MontageError)
24
+
25
+ # Raised when a creating a new project in an existing project directory.
26
+ ProjectExists = Class.new(MontageError)
27
+
28
+ # Raised when a sprite set expects a source image, but none could be found.
29
+ MissingSource = Class.new(MontageError)
30
+
31
+ # Raised when a sprite can't be saved due to incorrect permissions.
32
+ TargetNotWritable = Class.new(MontageError)
33
+ end
@@ -0,0 +1,32 @@
1
+ require 'highline/import'
2
+
3
+ HighLine.use_color = ! ARGV.delete('--no-color') && ! ARGV.delete('--no-colour')
4
+ HighLine.use_color = false if !STDOUT.tty? && !ENV.has_key?("AUTOTEST")
5
+
6
+ module Kernel
7
+ def_delegators :$terminal, :color
8
+ end
9
+
10
+ module Montage
11
+ module Commands
12
+ BLANK = "\n".freeze
13
+
14
+ extend self
15
+
16
+ # Prints the Montage masthead, introducing the programme, and including
17
+ # the current version number.
18
+ def print_masthead
19
+ say BLANK
20
+ say "Montage v#{Montage::VERSION}"
21
+ say "=========#{'=' * Montage::VERSION.length}"
22
+ say BLANK
23
+ end
24
+
25
+ # Exits immediately, outputting a blank line first.
26
+ def exit(status)
27
+ say BLANK
28
+ Kernel.exit(status)
29
+ end
30
+
31
+ end # Commands
32
+ end # Montage
@@ -0,0 +1,234 @@
1
+ module Montage
2
+ module Commands
3
+ # Generates sprites for a project.
4
+ class Generate
5
+ extend Commands
6
+
7
+ # Terminal reset code; removes any content on the current line and moves
8
+ # the cursor back to the beginning.
9
+ RESET = "\r\e[0K"
10
+
11
+ # Glyphs used when doing long-running processes.
12
+ GLYPHS = %w( ~ \\ | / )
13
+
14
+ # Given a project, generates sprites.
15
+ #
16
+ # @param [Array] argv
17
+ # The arguments given on the command line.
18
+ #
19
+ def self.run(argv)
20
+ new(Montage::Project.find(Dir.pwd), argv.include?('--force')).run!
21
+
22
+ rescue Montage::MissingProject
23
+ say color(<<-ERROR.compress_lines, :red)
24
+ Couldn't find a Montage project in the current directory. If
25
+ you want to create a new project here, run `montage init'.
26
+ ERROR
27
+
28
+ exit(1)
29
+
30
+ rescue Montage::MissingSource, Montage::TargetNotWritable => e
31
+ say Montage::Commands::BLANK
32
+ say color(e.message.compress_lines, :red)
33
+
34
+ exit(1)
35
+ end
36
+
37
+ # Creates a new Generate instance.
38
+ #
39
+ # @param [Montage::Project] project
40
+ # The project whose sprites are to be generated.
41
+ # @param [Boolean] force
42
+ # Rengerate sprites, even if they haven't been changed.
43
+ #
44
+ def initialize(project, force)
45
+ @project, @force, @generated = project, force, []
46
+ end
47
+
48
+ # Runs the generator, saving the sprites and cache.
49
+ #
50
+ def run!
51
+ if generate_sprites!
52
+ optimise_with_pngout!
53
+ write_cache!
54
+ write_sass!
55
+ end
56
+ end
57
+
58
+ private # ==============================================================
59
+
60
+ # Returns the cached digests.
61
+ #
62
+ # @return [Hash]
63
+ #
64
+ def cache
65
+ @_sprite_caches ||= begin
66
+ cache_path = @project.paths.sprites + '.montage_cache'
67
+ cache_path.file? ? YAML.load_file(cache_path) || {} : {}
68
+ end
69
+ end
70
+
71
+ # Step 1: Generates the sprites for the given project. Skips those which
72
+ # have not changed since they were last generated.
73
+ #
74
+ # @return [Boolean]
75
+ # Returns true if at least one sprite has been updated.
76
+ #
77
+ def generate_sprites!
78
+ unless @project.paths.sprites.directory?
79
+ @project.paths.sprites.mkpath
80
+ end
81
+
82
+ @project.sprites.each do |sprite|
83
+ digest = sprite.digest
84
+
85
+
86
+ if @force or cache[sprite.name] != digest or not sprite.path.file?
87
+ with_feedback %(Generating "#{sprite.name}"), 'Generating' do
88
+ sprite.write
89
+ cache[sprite.name] = digest
90
+ @generated << sprite
91
+ end
92
+
93
+ say color("Done", :green)
94
+ else
95
+ say %(- Generating "#{sprite.name}": ) +
96
+ color("Unchanged; ignoring", :yellow)
97
+ end
98
+ end
99
+
100
+ say Montage::Commands::BLANK
101
+ @generated.any?
102
+ end
103
+
104
+ # Step 2: Optimise generated sprites with PNGOut.
105
+ #
106
+ def optimise_with_pngout!
107
+ return if @generated.empty?
108
+
109
+ # Try to find PNGOut.
110
+ pngout = `which pngout pngout-darwin`.split("\n").first
111
+
112
+ if pngout.nil?
113
+ say <<-MESSAGE.compress_lines
114
+ Skipping optimisation with PNGOut since Montage couldn't find
115
+ "pngout" or "pngout-darwin" anywhere.
116
+ MESSAGE
117
+ say Montage::Commands::BLANK
118
+
119
+ return
120
+ end
121
+
122
+ max_sprite_name_length = @generated.inject(0) do |max, sprite|
123
+ max > sprite.name.length ? max : sprite.name.length
124
+ end
125
+
126
+ @generated.each do |sprite|
127
+ original_size = sprite.path.size
128
+
129
+ with_feedback %(Optimising "#{sprite.name}"), 'Optimising' do
130
+ 5.times do |i|
131
+ # Optimise until pngout reports that it can't compress further,
132
+ # or until we've tried five times.
133
+ out = `#{pngout} #{sprite.path} #{sprite.path} -s0 -k0 -y`
134
+ break if out =~ /Unable to compress further/
135
+ end
136
+ end
137
+
138
+ new_size = sprite.path.size
139
+
140
+ reduction = ('%.1fkb (%d' % [
141
+ (original_size.to_f - new_size) / 1024,
142
+ 100 - (new_size.to_f / original_size) * 100 ]) + '%)'
143
+
144
+ say color("Done; saved #{reduction}", :green)
145
+ end
146
+
147
+ say Montage::Commands::BLANK
148
+
149
+ rescue Errno::ENOENT
150
+ say ("#{RESET}" + <<-MESSAGE.compress_lines)
151
+ Skipping optimisation with PNGOut since Montage is currently only in
152
+ bed with Linux and OS X. Sorry!
153
+ MESSAGE
154
+ say Montage::Commands::BLANK
155
+ exit(1)
156
+ end
157
+
158
+ # Step 3: Writes the cached digests to the cache file.
159
+ #
160
+ def write_cache!
161
+ cache_path = @project.paths.sprites + '.montage_cache'
162
+
163
+ File.open(cache_path, 'w') do |cache_writer|
164
+ cache_writer.puts YAML.dump(cache)
165
+ end
166
+ end
167
+
168
+ # Step 4: Writes the Sass file to disk.
169
+ #
170
+ def write_sass!
171
+ unless @project.paths.sass == false
172
+ say "- Generating Sass: "
173
+ Montage::SassBuilder.new(@project).write
174
+ say color("Done", :green)
175
+ say Montage::Commands::BLANK
176
+ end
177
+ end
178
+
179
+ # --- Optimisation Output ----------------------------------------------
180
+
181
+ # Executes a block while providing live feedback to the user.
182
+ #
183
+ # @param [String] prefix
184
+ # The "prefix" of each notification; this is always shown at the
185
+ # beginning of each line.
186
+ # @param [String] verb
187
+ # What's happening?
188
+ #
189
+ # @example
190
+ # with_notification('Generating image', 'Generating') { ... }
191
+ #
192
+ # # "- Generating image: Generating" # initially
193
+ # # "- Generating image: Still generating" # after 3 seconds
194
+ # # "- Generating image: STILL generating" # after 6 seconds
195
+ #
196
+ def with_feedback(prefix, verb = 'Generating', &work)
197
+ notifier = Thread.new do
198
+ prefix = "- #{prefix}: "
199
+ iteration = 0
200
+
201
+ while true do
202
+ case iteration
203
+ when 0 then message = color(verb, :blue)
204
+ when 30 then message = color("Still #{verb.downcase}", :blue)
205
+ when 60 then message = color("STILL #{verb.downcase}", :blue)
206
+ when 90 then message =
207
+ color("Gosh, this is taking a while...", :blue)
208
+ end
209
+
210
+ say "#{RESET}#{prefix}#{message} [#{GLYPHS[iteration % 4]}] "
211
+
212
+ iteration += 1
213
+ $stdout.flush
214
+ sleep(0.1)
215
+ end
216
+ end
217
+
218
+ worker = Thread.new do
219
+ begin
220
+ work.call
221
+ ensure
222
+ notifier.kill
223
+ end
224
+ end
225
+
226
+ notifier.join
227
+ worker.join
228
+
229
+ say "#{RESET}#{prefix}"
230
+ end
231
+
232
+ end # Generate
233
+ end # Commands
234
+ end # Montage