spriteous 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/LICENSE +14 -0
  2. data/README.md +61 -0
  3. data/lib/spriteous.rb +81 -0
  4. metadata +59 -0
data/LICENSE ADDED
@@ -0,0 +1,14 @@
1
+ Copyright (C) 2012 Donnie Akers
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ You should have received a copy of the GNU General Public License
14
+ along with this program. If not, see http://www.gnu.org/licenses.
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ ### Description
2
+ Spriteous is a righteous sprite extraction utility. It uses a "reverse flood fill" algorithm to naively obtain each individual sprite from a spritesheet, "sprite" here defined as a contiguous group of pixels which do not match the background color (assumed to be the first pixel) of the sheet.
3
+
4
+ ### Installation
5
+ gem install spriteous
6
+
7
+ ### Usage
8
+ Spriteous currently uses the ChunkyPNG library (more specifically, the oily_png C extension), so only that format is supported at this time. On the bright side, this means that a Spriteous instance can easily be initialized either from a filename or raw PNG data:
9
+
10
+ ```ruby
11
+ Spriteous.new('sheet.png')
12
+ Spriteous.new(File.read 'sheet.png')
13
+ mario = Spriteous.new(` curl -s http://i.imgur.com/Fmd9q.png `)
14
+ ```
15
+
16
+ **#extract** is where the magic happens. If called with no parameters, it simply returns all of the extracted sprites as an array of ChunkyPNG::Images.
17
+
18
+ ```ruby
19
+ sprites = mario.extract
20
+ sprites.size # => 85
21
+ sprites.each_with_index do |sprite, i|
22
+ puts "Sprite #{i} contains #{sprite.palette.size} colors."
23
+ end
24
+ ```
25
+
26
+ If you bothered to count the sprites in the linked image, you might have noticed that #size's value is unfortunately a little off. Specifically, it's over by 4, and [zooming way in](http://i.imgur.com/l9xE5.png) reveals why this is the case. The algorithm used relies on all of a sprite's pixels being contiguous, but those damned fireballs just *have* to have disjointed pixels.
27
+
28
+ Alas, this seems to be an unavoidable consequence of the flood fill algorithm, and I think a major overhaul would be required to properly handle these edge cases. Of course, then the algorithm would break completely on sheets where the sprites are positioned very closely together. The only solution would be to implement content-awareness, but that is beyond the scope of this project, and‒given the plethora of potential sprite styles and sheet layouts‒perhaps even all of computer vision. ("If you seek advice, compel people to prove you wrong.")
29
+
30
+ Spriteous compromises by allowing for a minimum square pixel value to be passed to #extract.
31
+
32
+ ```ruby
33
+ mario.extract(min_size: 2).size # => 81
34
+ ```
35
+
36
+ Lossy sprite extraction is unpleasant, sure, but it's at least a slight improvement over grabbing lots of "junk" sprites.
37
+
38
+ ##### Saving
39
+
40
+ More often than not, the intention will simply be to save the extracted sprites rather than do something with them individually. While you could use #each_with_index to implement this functionality, it's much saner to just pass the *save_format* option to #extract.
41
+
42
+ ```ruby
43
+ mario.extract(min_size: 2, save_format: 'sprites/mario/%02d.png')
44
+ ```
45
+
46
+ Saving a collection of sprites by index is pretty much the only reasonable way to do it, so some form of `%d` must be present, but that's the only hard-and-fast requirement.
47
+
48
+ ### Roadmap
49
+ Spriteous was predominantly created to assist in a personal project of mine, and it has served that purpose quite well. It certainly caters to a relatively small niche, as the scarcity of available options would indicate, but during my research I came upon several people seeking such a tool, only to be told (on spriting-centric forums, no less!) that their only option was to manually crop. If nothing else, I hope those previously lost souls find their way here.
50
+
51
+ As is the case in any creative endeavor, this project's growth would please its designer immensely. Spriteous is by no means feature-complete, and indeed it very likely tackles the problem in a non-optimal way. Following is a list of my own criticisms and potential avenues for improvement.
52
+
53
+ ##### TODO
54
+ * Implement a better algorithm. Flood fill was nice and easy, but there are numerous ways to improve upon it. I've entertained the idea of using a moving box to determine the edges of a sprite, but irregular dimensions and transparency make this difficult.
55
+ * Support, at the very least, JPG and GIF formats, likely via RMagick.
56
+ * Improve speed when working with large spritesheets, probably by avoiding checking the entire image for the next non-transparent pixel when looking for the next sprite, though how to do this presently escapes me.
57
+ * Cleverly determine when the background color has changed, as is a common occurrence between sections.
58
+ * Attempt to remove non-sprite portions of a sheet like the ripper's tag and irrelevant text, instead of it being a manual step for the user.
59
+
60
+ ##### Contributing
61
+ Comments, criticisms, and code are all heartily welcome.
data/lib/spriteous.rb ADDED
@@ -0,0 +1,81 @@
1
+ #coding: utf-8
2
+ require 'fileutils'
3
+ require 'oily_png'
4
+
5
+ class Spriteous
6
+ def initialize(data)
7
+ # Allow for creation from either raw PNG data or a file.
8
+ meth = data[0] == "\x89" ? :from_string : :from_file
9
+ @img = ChunkyPNG::Image.send meth, data
10
+
11
+ back, @w = @img.pixels.first, @img.width
12
+
13
+ # The original image's pixels get modified in place to erase the background
14
+ # color, They're duplicated because the algorithm relies on "erasing" found
15
+ # pixels, and we eventually need the originals to construct the sprites.
16
+ @pixels = @img.pixels.map! { |p| p == back ? 0 : p }.dup
17
+ end
18
+
19
+ # A "reverse flood fill" algorithm for finding all of the pixels that belong
20
+ # to a given sprite. Rather than finding and replacing background pixels, it
21
+ # looks for a contiguous path starting from the first non-transparent pixel.
22
+ def traverse(pixel)
23
+ queue = [pixel]
24
+
25
+ # Used for checking the target's adjacent pixels. All eight directions are
26
+ # checked to avoid missing single diagonal pixels at the edges.
27
+ neighbors = [-@w - 1, -@w, -@w + 1, -1, 1, @w - 1, @w, @w + 1]
28
+
29
+ until queue.empty?
30
+ p = queue.pop
31
+
32
+ if @pixels[p] > 0
33
+ # Store this non-transparent pixel's index and then make it transparent
34
+ # to avoid unnecessarily checking it again from a neighbor.
35
+ @pixels[(@found << p).last] = 0
36
+
37
+ # Apply the eight cardinal directions to the current pixel's index and
38
+ # prepare them to be checked on the next iteration.
39
+ queue.concat neighbors.map { |n| p + n }
40
+ end
41
+ end
42
+ end
43
+
44
+ # Entry point for the traverse method defined above. Returns array of sprites
45
+ # (instances of ChunkyPNG::Image) if called without the argument. save_format
46
+ # should be a format string that specifies %d in some way, as this is used to
47
+ # generate the names of the ripped sprites. Example: 'sprites/mario/%03d.png'
48
+ def extract(opts = {})
49
+ sprites = []
50
+
51
+ # Sprites are erased as they're found, so we're done when the entire image
52
+ # is comprised of a single color value, 0 (transparent) in this case.
53
+ while @pixels.uniq.size > 1
54
+ @found = []
55
+
56
+ # Find the next sprite (first non-transparent pixel) and traverse from it
57
+ # until there is no contiguous direction to continue in. @found will then
58
+ # contain all of the pixel indices for the current sprite.
59
+ traverse @pixels.find_index { |p| p > 0 }
60
+
61
+ # @found contains pixel indices, but now we need coordinate data.
62
+ x, y = @found.map { |f| f % @w }, @found.map { |f| f / @w }
63
+
64
+ # Determine the sprite's bounding box (top, left, width, height), crop it
65
+ # from the original image, and push it to the collection unless a minimum
66
+ # size is desired and the resulting sprite's dimensions don't satisfy it.
67
+ box = [x.min, y.min, (w = x.max - x.min + 1), (h = y.max - y.min + 1)]
68
+ sprites << @img.crop(*box) if w * h >= opts[:min_size].to_i
69
+ end
70
+
71
+ return sprites unless opts[:save_format]
72
+
73
+ unless opts[:save_format].include? '%d'
74
+ raise ArgumentError, "%d is required to properly save extracted sprites."
75
+ end
76
+
77
+ dir = opts[:save_format].split('/')[0..-2].join '/'
78
+ FileUtils.mkdir_p dir unless dir.empty?
79
+ sprites.each_with_index { |s, i| s.save opts[:save_format] % i }
80
+ end
81
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spriteous
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Donnie Akers
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: oily_png
16
+ requirement: &75064140 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *75064140
25
+ description:
26
+ email:
27
+ - andkerosine@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - LICENSE
33
+ - README.md
34
+ - lib/spriteous.rb
35
+ homepage:
36
+ licenses: []
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ none: false
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubyforge_project:
55
+ rubygems_version: 1.8.15
56
+ signing_key:
57
+ specification_version: 3
58
+ summary: Naively separates a well-formatted spritesheet into its component parts.
59
+ test_files: []