spriteous 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +14 -0
- data/README.md +61 -0
- data/lib/spriteous.rb +81 -0
- 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: []
|