css-spriter 0.9.2

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 (70) hide show
  1. data/.gitignore +12 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +81 -0
  4. data/Rakefile +45 -0
  5. data/VERSION +1 -0
  6. data/bin/css-spriter +45 -0
  7. data/bin/png_info +61 -0
  8. data/examples/filter_util.rb +17 -0
  9. data/examples/sprites/.mtimes +12 -0
  10. data/examples/sprites/README +5 -0
  11. data/examples/sprites/fragment.css +0 -0
  12. data/examples/sprites/index.html +26 -0
  13. data/examples/sprites/many_sized_cats/.mtimes +3 -0
  14. data/examples/sprites/many_sized_cats/cat-on-keyboard.png +0 -0
  15. data/examples/sprites/many_sized_cats/darth_cat.png +0 -0
  16. data/examples/sprites/many_sized_cats/fragment.css +21 -0
  17. data/examples/sprites/many_sized_cats/music-keyboard-cat.png +0 -0
  18. data/examples/sprites/many_sized_cats/sprite.css +21 -0
  19. data/examples/sprites/many_sized_cats/sprite.png +0 -0
  20. data/examples/sprites/server.rb +10 -0
  21. data/examples/sprites/sprite.css +49 -0
  22. data/examples/sprites/words/.mtimes +4 -0
  23. data/examples/sprites/words/fragment.css +28 -0
  24. data/examples/sprites/words/latitude.png +0 -0
  25. data/examples/sprites/words/of.png +0 -0
  26. data/examples/sprites/words/set.png +0 -0
  27. data/examples/sprites/words/specified.png +0 -0
  28. data/examples/sprites/words/sprite.css +28 -0
  29. data/examples/sprites/words/sprite.png +0 -0
  30. data/init.rb +1 -0
  31. data/lib/css-spriter.rb +16 -0
  32. data/lib/css-spriter/directory_processor.rb +94 -0
  33. data/lib/css-spriter/image_data.rb +128 -0
  34. data/lib/css-spriter/mtime_tracker.rb +79 -0
  35. data/lib/css-spriter/png/chunk.rb +12 -0
  36. data/lib/css-spriter/png/file_header.rb +7 -0
  37. data/lib/css-spriter/png/filters.rb +80 -0
  38. data/lib/css-spriter/png/idat.rb +26 -0
  39. data/lib/css-spriter/png/iend.rb +9 -0
  40. data/lib/css-spriter/png/ihdr.rb +34 -0
  41. data/lib/css-spriter/png/image.rb +153 -0
  42. data/lib/css-spriter/png/parser.rb +54 -0
  43. data/lib/css-spriter/processor.rb +28 -0
  44. data/lib/css-spriter/sprite.rb +39 -0
  45. data/lib/css-spriter/stylesheet_builder.rb +22 -0
  46. data/spec/builders/image_builder.rb +23 -0
  47. data/spec/css_fragments/deep/style/fragment.css +1 -0
  48. data/spec/css_fragments/some/fragment.css +1 -0
  49. data/spec/expected_output/merge_right_test.png +0 -0
  50. data/spec/expected_output/write_test.png +0 -0
  51. data/spec/image_data_spec.rb +63 -0
  52. data/spec/images/lightening.png +0 -0
  53. data/spec/integration_spec.rb +151 -0
  54. data/spec/lib/file_header_spec.rb +10 -0
  55. data/spec/lib/idat_spec.rb +31 -0
  56. data/spec/lib/ihdr_spec.rb +43 -0
  57. data/spec/lib/image_spec.rb +42 -0
  58. data/spec/lib/parser_spec.rb +12 -0
  59. data/spec/lib/sprite_spec.rb +39 -0
  60. data/spec/mtime_tracking_spec.rb +69 -0
  61. data/spec/spec.opts +1 -0
  62. data/spec/spec_helper.rb +17 -0
  63. data/spec/sprite_dirs/words/latitude.png +0 -0
  64. data/spec/sprite_dirs/words/of.png +0 -0
  65. data/spec/sprite_dirs/words/set.png +0 -0
  66. data/spec/sprite_dirs/words/specified.png +0 -0
  67. data/spec/tmp/merge_right_test.png +0 -0
  68. data/spec/tmp/write_test.png +0 -0
  69. data/tasks/spriter_tasks.rake +25 -0
  70. metadata +148 -0
@@ -0,0 +1,4 @@
1
+ ./words/set.png 1258120615
2
+ ./words/latitude.png 1258120615
3
+ ./words/specified.png 1258120615
4
+ ./words/of.png 1258120615
@@ -0,0 +1,28 @@
1
+ .words_latitude {
2
+ background: transparent url(/words/sprite.png) 0px 0px no-repeat;
3
+ width:66;
4
+ height:21;
5
+ text-indent:-5000px;
6
+ }
7
+
8
+ .words_of {
9
+ background: transparent url(/words/sprite.png) -66px 0px no-repeat;
10
+ width:19;
11
+ height:21;
12
+ text-indent:-5000px;
13
+ }
14
+
15
+ .words_set {
16
+ background: transparent url(/words/sprite.png) -85px 0px no-repeat;
17
+ width:26;
18
+ height:21;
19
+ text-indent:-5000px;
20
+ }
21
+
22
+ .words_specified {
23
+ background: transparent url(/words/sprite.png) -111px 0px no-repeat;
24
+ width:70;
25
+ height:21;
26
+ text-indent:-5000px;
27
+ }
28
+
Binary file
Binary file
Binary file
@@ -0,0 +1,28 @@
1
+ .words_of {
2
+ background: transparent url(/examples/sprites/words/sprite.png) -66px 0px no-repeat;
3
+ width:19;
4
+ height:21;
5
+ text-indent:-5000px;
6
+ }
7
+
8
+ .words_set {
9
+ background: transparent url(/examples/sprites/words/sprite.png) -85px 0px no-repeat;
10
+ width:26;
11
+ height:21;
12
+ text-indent:-5000px;
13
+ }
14
+
15
+ .words_specified {
16
+ background: transparent url(/examples/sprites/words/sprite.png) -111px 0px no-repeat;
17
+ width:70;
18
+ height:21;
19
+ text-indent:-5000px;
20
+ }
21
+
22
+ .words_latitude {
23
+ background: transparent url(/examples/sprites/words/sprite.png) 0px 0px no-repeat;
24
+ width:66;
25
+ height:21;
26
+ text-indent:-5000px;
27
+ }
28
+
Binary file
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ # Include hook code here
@@ -0,0 +1,16 @@
1
+ require 'zlib'
2
+
3
+ require 'css-spriter/png/file_header'
4
+ require 'css-spriter/png/parser'
5
+ require 'css-spriter/png/filters'
6
+ require 'css-spriter/png/chunk'
7
+ require 'css-spriter/png/ihdr'
8
+ require 'css-spriter/png/idat'
9
+ require 'css-spriter/png/iend'
10
+ require 'css-spriter/png/image'
11
+ require 'css-spriter/sprite'
12
+ require 'css-spriter/directory_processor'
13
+ require 'css-spriter/stylesheet_builder'
14
+ require 'css-spriter/processor'
15
+ require 'css-spriter/mtime_tracker'
16
+ require 'css-spriter/image_data'
@@ -0,0 +1,94 @@
1
+ class DirectoryProcessor
2
+
3
+ DEFAULT_TEMPLATE = <<-EOF
4
+ .<name>_<image_name> {
5
+ background: transparent url(<image_loc>) <offset>px 0px no-repeat;
6
+ width:<width>;
7
+ height:<height>;
8
+ text-indent:-5000px;
9
+ }
10
+
11
+ EOF
12
+
13
+ def initialize(dir, options = {})
14
+ @options = options
15
+ @dir = dir
16
+ @sprite = Sprite.new
17
+ @tracker = MtimeTracker.new(@dir,
18
+ :exclude => ['sprite.css', 'fragment.css', 'sprite.png'])
19
+ end
20
+
21
+ def images
22
+ Dir.glob(@dir + "/*.png").reject{|i| i.match /sprite\.png/}
23
+ end
24
+
25
+ def write
26
+ return unless @tracker.has_changes?
27
+ images.each {|f| @sprite.append(PNG::Image.image_data(f))}
28
+ @sprite.write(sprite_file)
29
+ File.open(css_file, 'w') do |f|
30
+ f.write(css)
31
+ end
32
+ @tracker.update
33
+ end
34
+
35
+ def cleanup
36
+ File.delete(sprite_file) rescue nil
37
+ File.delete(css_file) rescue nil
38
+ @tracker.cleanup
39
+ end
40
+
41
+ def sprite_file
42
+ @dir + "/sprite.png"
43
+ end
44
+
45
+ def css_file
46
+ @dir + "/fragment.css"
47
+ end
48
+
49
+ def dir_name
50
+ @dir.split('/').last
51
+ end
52
+
53
+ def image_loc
54
+ #TODO: Lame!
55
+
56
+ dir = truncate_abs_path
57
+ base = ("/" + dir + "/sprite.png").gsub(/^\/.\//, "/").gsub("//", "/")
58
+ source = @options[:source]
59
+ base = base.gsub(source, "") if source && source != "."
60
+ base = @options[:path_prefix] + base if @options[:path_prefix]
61
+ base
62
+ end
63
+
64
+ def truncate_abs_path
65
+ return @dir unless @options[:source]
66
+ path_elements = @options[:source].split('/')
67
+ path_elements.pop #we want to remove everything above the root
68
+ to_truncate = path_elements.join("/")
69
+ @dir.gsub(to_truncate, "")
70
+ end
71
+
72
+ def template_file
73
+ @dir + "/template.css"
74
+ end
75
+
76
+ def template
77
+ if File.exists?(template_file)
78
+ return File.read(template_file)
79
+ end
80
+ return DEFAULT_TEMPLATE
81
+ end
82
+
83
+ def css
84
+ @sprite.locations.inject("") do |out, image|
85
+ image_name, properties = image
86
+ out << template.gsub("<name>", dir_name).
87
+ gsub("<image_name>", image_name.to_s).
88
+ gsub("<width>", properties[:width].to_s).
89
+ gsub("<height>", properties[:height].to_s).
90
+ gsub("<offset>", properties[:x].to_s).
91
+ gsub("<image_loc>", image_loc)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,128 @@
1
+ require 'enumerator'
2
+
3
+ module CssSpriter
4
+ class ImageData
5
+ RGB_WIDTH = 3
6
+ RGBA_WIDTH = 4
7
+
8
+ def initialize(options = {})
9
+ @data = (options.delete :data) || []
10
+ @properties = options
11
+ end
12
+
13
+ def name; @properties[:name] || "default"; end
14
+ def scanline_width; @properties[:scanline_width]; end
15
+ def width; scanline_width / pixel_width; end
16
+ def pixel_width; @properties[:pixel_width]; end
17
+
18
+ # need better checks, because currently compatible is
19
+ # similar color type, or depth.. maybe it doesn't matter...
20
+ def compatible?(image)
21
+ self.pixel_width == image.pixel_width
22
+ end
23
+
24
+ def to_s
25
+ "#{name} pixel width: #{pixel_width}"
26
+ end
27
+
28
+ def last_scanline(idx)
29
+ last_row_index = idx - 1
30
+ (last_row_index < 0 ? [] : @data[last_row_index])
31
+ end
32
+
33
+ def merge_left( other )
34
+ merged = ImageData.new(@properties.merge(:scanline_width => self.scanline_width + other.scanline_width,
35
+ :name => "#{self.name}_#{other.name}"))
36
+ other.each_with_index do |row, idx|
37
+ merged[idx] = row + self[idx]
38
+ end
39
+ merged
40
+ end
41
+
42
+ def fill_to_height( desired_height )
43
+ return self if desired_height == height
44
+
45
+ img = ImageData.new(@properties.merge(:data => @data.clone))
46
+ empty_row = [0] * ( scanline_width )
47
+
48
+ ( desired_height - height ).times do
49
+ img << empty_row
50
+ end
51
+ img
52
+ end
53
+
54
+ def to_rgba( alpha_value=255 )
55
+ # test that conversion updates pixel width, scanline width
56
+
57
+ return self if pixel_width == RGBA_WIDTH
58
+
59
+ # so ruby 1.9 has different ideas then 1.8 on how Enumerable should work
60
+ # 1.9 returns Enumerable object if no block given
61
+ # 1.8 complains if no block, unless using enum_x methods
62
+ slice_method = (RUBY_VERSION.include?( "1.9" )) ? :each_slice : :enum_slice
63
+
64
+ rgba_data = @data.map do |row|
65
+ pixels = row.send( slice_method, RGB_WIDTH )
66
+ pixels.inject([]){|result, pixel| result + pixel + [alpha_value] }
67
+ end
68
+
69
+ ImageData.new( :data => rgba_data,
70
+ :pixel_width => 4,
71
+ :scanline_width => (@properties[:scanline_width] / RGB_WIDTH) * RGBA_WIDTH,
72
+ :name => name)
73
+ end
74
+
75
+ def [](row)
76
+ @data[row]
77
+ end
78
+
79
+ def []=(idx, row_data)
80
+ @data[idx] = row_data
81
+ end
82
+
83
+ def scanline(row)
84
+ self[row]
85
+ end
86
+
87
+ def empty?
88
+ @data.empty?
89
+ end
90
+
91
+ def height
92
+ size
93
+ end
94
+
95
+ def size
96
+ @data.size
97
+ end
98
+
99
+ def last
100
+ @data.last
101
+ end
102
+
103
+ def <<(row)
104
+ @data << row
105
+ self
106
+ end
107
+
108
+ def each(&block)
109
+ @data.each &block
110
+ end
111
+
112
+ def each_with_index(&block)
113
+ @data.each_with_index(&block)
114
+ end
115
+
116
+ def flatten!
117
+ @data.flatten!
118
+ end
119
+
120
+ def pack(*args)
121
+ @data.pack(*args)
122
+ end
123
+
124
+ def == (other)
125
+ @data == other
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,79 @@
1
+ class MtimeTracker
2
+ def initialize(dir, options = {})
3
+ @dir = dir
4
+ @options = options
5
+ end
6
+
7
+ def cleanup
8
+ File.delete(mtime_file) rescue nil
9
+ end
10
+
11
+ def fresh?
12
+ not File.exists?(mtime_file)
13
+ end
14
+
15
+ def files
16
+ return @files if @files
17
+ @files = without_exclusions(Dir.glob(@dir + "/**/*"))
18
+ end
19
+
20
+ def without_exclusions(files)
21
+ return files unless @options[:exclude]
22
+ exclusions = [@options[:exclude]].flatten
23
+ files.select{|f| not exclusions.any?{|e| f.match e}}
24
+ end
25
+
26
+ def current_mtimes
27
+ @current ||= files.inject({}) do |map, f|
28
+ map[f] = File.mtime(f).to_i; map
29
+ end
30
+ end
31
+
32
+ def file_changed?(file)
33
+ mtimes[file] != current_mtimes[file]
34
+ end
35
+
36
+ def mtimes
37
+ return @mtimes if @mtimes
38
+ return {} unless File.exists?(mtime_file)
39
+ @mtimes = read_mtimes
40
+ end
41
+
42
+ def read_mtimes
43
+ mtimes = {}
44
+ File.open(mtime_file) do |f|
45
+ f.each do |line|
46
+ name, time = line.split("\t")
47
+ mtimes[name] = time.to_i
48
+ end
49
+ end
50
+ mtimes
51
+ end
52
+
53
+ def changeset
54
+ files.select{|f| file_changed?(f)}
55
+ end
56
+
57
+ def has_changes?
58
+ not changeset.empty?
59
+ end
60
+
61
+ def mtime_file
62
+ @dir + "/.mtimes"
63
+ end
64
+
65
+ def update
66
+ File.open(mtime_file, 'w') do |f|
67
+ current = current_mtimes
68
+ flat = current.map{|k, v| "#{k}\t#{v}\n"}.join
69
+ f.write flat
70
+ end
71
+ reset
72
+ end
73
+
74
+ def reset
75
+ @mtimes = nil
76
+ @current = nil
77
+ @files = nil
78
+ end
79
+ end
@@ -0,0 +1,12 @@
1
+ module PNG
2
+ class Chunk
3
+ def chunk_name
4
+ raise "looks like you havn't subclassed and defined a chunk_name"
5
+ end
6
+
7
+ def to_chunk
8
+ to_check = chunk_name + encode
9
+ [encode.length].pack("N") + to_check + [Zlib.crc32(to_check)].pack("N")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,7 @@
1
+ module PNG
2
+ class FileHeader
3
+ def encode
4
+ [137, 80, 78, 71, 13, 10, 26, 10].pack("C*")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,80 @@
1
+ module PNG
2
+ class Filters
3
+ class << self
4
+ def fetch_pixel(idx, row)
5
+ idx < 0 ? 0 : row[idx] || 0
6
+ end
7
+
8
+ #filter methods are inlined here for performance
9
+ def decode( filter_type, value, index, row, last_row, record_width )
10
+ case filter_type
11
+ when 0
12
+ #no filter
13
+ value
14
+ when 1
15
+ #left
16
+ (value + fetch_pixel(index - record_width, row)) % 256
17
+ when 2
18
+ #left
19
+ (value + fetch_pixel(index, last_row)) % 256
20
+ when 3
21
+ #avg
22
+ (value + ( (fetch_pixel(index - record_width, row) + fetch_pixel(index, last_row)) / 2 ).floor) % 256
23
+ when 4
24
+ #paeth
25
+ left = fetch_pixel(index - record_width, row)
26
+ above = fetch_pixel(index, last_row)
27
+ upper_left = fetch_pixel(index - record_width, last_row)
28
+
29
+ pr = paeth_predictor( left, above, upper_left )
30
+
31
+ (value + pr) % 256
32
+ else
33
+ raise "Invalid filter type (#{filter_type})"
34
+ end
35
+ end
36
+ def encode( filter_type, value, index, row, last_row, record_width )
37
+ case filter_type
38
+ when 0
39
+ value
40
+ when 1
41
+ #left
42
+ (value - fetch_pixel(index - record_width, row)) % 256
43
+ when 2
44
+ #up
45
+ (value - fetch_pixel(index, last_row)) % 256
46
+ when 3
47
+ #avg
48
+ (value - ( (fetch_pixel(index - record_width, row) + fetch_pixel(index, last_row)) / 2 ).floor) % 256
49
+ when 4
50
+ #paeth
51
+ left = fetch_pixel(index - record_width, row)
52
+ above = fetch_pixel(index, last_row)
53
+ upper_left = fetch_pixel(index - record_width, last_row)
54
+
55
+ pr = paeth_predictor( left, above, upper_left )
56
+ (value - pr) % 256
57
+ else
58
+ raise "Filter type (#{filter_type}) is not supported"
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def paeth_predictor( left, above, upper_left )
65
+ p = left + above - upper_left
66
+ pa = (p - left).abs
67
+ pb = (p - above).abs
68
+ pc = (p - upper_left).abs
69
+
70
+ pr = upper_left
71
+ if ( pa <= pb and pa <= pc)
72
+ pr = left
73
+ elsif (pb <= pc)
74
+ pr = above
75
+ end
76
+ pr
77
+ end
78
+ end
79
+ end
80
+ end