chunky_png 0.0.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/README.rdoc +20 -10
  2. data/chunky_png.gemspec +18 -6
  3. data/lib/chunky_png.rb +11 -28
  4. data/lib/chunky_png/canvas.rb +186 -0
  5. data/lib/chunky_png/canvas/adam7_interlacing.rb +53 -0
  6. data/lib/chunky_png/canvas/drawing.rb +8 -0
  7. data/lib/chunky_png/{pixel_matrix → canvas}/operations.rb +12 -12
  8. data/lib/chunky_png/canvas/png_decoding.rb +145 -0
  9. data/lib/chunky_png/canvas/png_encoding.rb +182 -0
  10. data/lib/chunky_png/chunk.rb +101 -23
  11. data/lib/chunky_png/color.rb +307 -0
  12. data/lib/chunky_png/datastream.rb +143 -45
  13. data/lib/chunky_png/image.rb +31 -30
  14. data/lib/chunky_png/palette.rb +49 -47
  15. data/lib/chunky_png/rmagick.rb +43 -0
  16. data/spec/chunky_png/canvas/adam7_interlacing_spec.rb +108 -0
  17. data/spec/chunky_png/canvas/png_decoding_spec.rb +81 -0
  18. data/spec/chunky_png/canvas/png_encoding_spec.rb +70 -0
  19. data/spec/chunky_png/canvas_spec.rb +71 -0
  20. data/spec/chunky_png/color_spec.rb +104 -0
  21. data/spec/chunky_png/datastream_spec.rb +32 -0
  22. data/spec/chunky_png/image_spec.rb +25 -0
  23. data/spec/chunky_png/rmagick_spec.rb +21 -0
  24. data/spec/{integration/image_spec.rb → chunky_png_spec.rb} +14 -8
  25. data/spec/resources/composited.png +0 -0
  26. data/spec/resources/cropped.png +0 -0
  27. data/spec/resources/damaged_chunk.png +0 -0
  28. data/spec/resources/damaged_signature.png +13 -0
  29. data/spec/resources/pixelstream.rgba +67 -0
  30. data/spec/resources/replaced.png +0 -0
  31. data/spec/resources/text_chunk.png +0 -0
  32. data/spec/resources/ztxt_chunk.png +0 -0
  33. data/spec/spec_helper.rb +8 -5
  34. data/tasks/github-gem.rake +1 -1
  35. metadata +37 -18
  36. data/lib/chunky_png/pixel.rb +0 -272
  37. data/lib/chunky_png/pixel_matrix.rb +0 -136
  38. data/lib/chunky_png/pixel_matrix/decoding.rb +0 -159
  39. data/lib/chunky_png/pixel_matrix/encoding.rb +0 -89
  40. data/spec/unit/decoding_spec.rb +0 -83
  41. data/spec/unit/encoding_spec.rb +0 -27
  42. data/spec/unit/pixel_matrix_spec.rb +0 -93
  43. data/spec/unit/pixel_spec.rb +0 -47
@@ -0,0 +1,307 @@
1
+ module ChunkyPNG
2
+
3
+ # The Color module defines methods for handling colors. Within the ChunkyPNG
4
+ # library, the concepts of pixels and colors are both used, and they are
5
+ # both represented by a Fixnum.
6
+ #
7
+ # Pixels/colors are represented in RGBA componetns. Each of the four
8
+ # components is stored with a depth of 8 bits (maximum value = 255 =
9
+ # {ChunkyPNG::Color::MAX}). Together, these components are stored in a 4-byte
10
+ # Fixnum.
11
+ #
12
+ # A color will always be represented using these 4 components in memory.
13
+ # When the image is encoded, a more suitable representation can be used
14
+ # (e.g. rgb, grayscale, palette-based), for which several conversion methods
15
+ # are provided in this module.
16
+ module Color
17
+ extend self
18
+
19
+ # The maximum value of each color component.
20
+ MAX = 0xff
21
+
22
+ ####################################################################
23
+ # CONSTRUCTING COLOR VALUES
24
+ ####################################################################
25
+
26
+ # Creates a new color using an r, g, b triple and an alpha value.
27
+ # @return [Fixnum] The newly constructed color value.
28
+ def rgba(r, g, b, a)
29
+ r << 24 | g << 16 | b << 8 | a
30
+ end
31
+
32
+ # Creates a new color using an r, g, b triple.
33
+ # @return [Fixnum] The newly constructed color value.
34
+ def rgb(r, g, b, a = MAX)
35
+ rgba(r, g, b, a)
36
+ end
37
+
38
+ # Creates a new color using a grayscale teint.
39
+ # @return [ChunkyPNG::Color] The newly constructed color value.
40
+ def grayscale(teint, a = MAX)
41
+ rgba(teint, teint, teint, a)
42
+ end
43
+
44
+ # Creates a new color using a grayscale teint and alpha value.
45
+ # @return [Fixnum] The newly constructed color value.
46
+ def grayscale_alpha(teint, a)
47
+ rgba(teint, teint, teint, a)
48
+ end
49
+
50
+ ####################################################################
51
+ # COLOR IMPORTING
52
+ ####################################################################
53
+
54
+ # Creates a color by unpacking an rgb triple from a string.
55
+ #
56
+ # @param [String] stream The string to load the color from. It should be
57
+ # at least 3 + pos bytes long.
58
+ # @param [Fixnum] pos The position in the string to load the triple from.
59
+ # @return [Fixnum] The newly constructed color value.
60
+ def from_rgb_stream(stream, pos = 0)
61
+ rgb(*stream.unpack("@#{pos}C3"))
62
+ end
63
+
64
+ # Creates a color by unpacking an rgba triple from a string
65
+ #
66
+ # @param [String] stream The string to load the color from. It should be
67
+ # at least 4 + pos bytes long.
68
+ # @param [Fixnum] pos The position in the string to load the triple from.
69
+ # @return [Fixnum] The newly constructed color value.
70
+ def from_rgba_stream(stream, pos = 0)
71
+ rgba(*stream.unpack("@#{pos}C4"))
72
+ end
73
+
74
+ # Creates a color by converting it from a string in hex notation.
75
+ #
76
+ # It supports colors with (#rrggbbaa) or without (#rrggbb) alpha channel.
77
+ # Color strings may include the prefix "0x" or "#".
78
+ #
79
+ # @param [String] str The color in hex notation. @return [Fixnum] The
80
+ # converted color value.
81
+ def from_hex(str)
82
+ case str
83
+ when /^(?:#|0x)?([0-9a-f]{6})$/i then ($1.hex << 8) | 0xff
84
+ when /^(?:#|0x)?([0-9a-f]{8})$/i then $1.hex
85
+ else raise "Not a valid hex color notation: #{str.inspect}!"
86
+ end
87
+ end
88
+
89
+ ####################################################################
90
+ # PROPERTIES
91
+ ####################################################################
92
+
93
+ # Returns the red-component from the color value.
94
+ #
95
+ # @param [Fixnum] value The color value.
96
+ # @return [Fixnum] A value between 0 and MAX.
97
+ def r(value)
98
+ (value & 0xff000000) >> 24
99
+ end
100
+
101
+ # Returns the green-component from the color value.
102
+ #
103
+ # @param [Fixnum] value The color value.
104
+ # @return [Fixnum] A value between 0 and MAX.
105
+ def g(value)
106
+ (value & 0x00ff0000) >> 16
107
+ end
108
+
109
+ # Returns the blue-component from the color value.
110
+ #
111
+ # @param [Fixnum] value The color value.
112
+ # @return [Fixnum] A value between 0 and MAX.
113
+ def b(value)
114
+ (value & 0x0000ff00) >> 8
115
+ end
116
+
117
+ # Returns the alpha channel value for the color value.
118
+ #
119
+ # @param [Fixnum] value The color value.
120
+ # @return [Fixnum] A value between 0 and MAX.
121
+ def a(value)
122
+ value & 0x000000ff
123
+ end
124
+
125
+ # Returns true if this color is fully opaque.
126
+ #
127
+ # @param [Fixnum] value The color to test.
128
+ # @return [true, false] True if the alpha channel equals MAX.
129
+ def opaque?(value)
130
+ a(value) == 0x000000ff
131
+ end
132
+
133
+ # Returns true if this color is fully transparent.
134
+ #
135
+ # @param [Fixnum] value The color to test.
136
+ # @return [true, false] True if the r, g and b component are equal.
137
+ def grayscale?(value)
138
+ r(value) == b(value) && b(value) == g(value)
139
+ end
140
+
141
+ # Returns true if this color is fully transparent.
142
+ #
143
+ # @param [Fixnum] value The color to test.
144
+ # @return [true, false] True if the alpha channel equals 0.
145
+ def fully_transparent?(value)
146
+ a(value) == 0x00000000
147
+ end
148
+
149
+ ####################################################################
150
+ # ALPHA COMPOSITION
151
+ ####################################################################
152
+
153
+ # Multiplies two fractions using integer math, where the fractions are stored using an
154
+ # integer between 0 and 255. This method is used as a helper method for compositing
155
+ # colors using integer math.
156
+ #
157
+ # This is a quicker implementation of ((a * b) / 255.0).round.
158
+ #
159
+ # @param [Fixnum] a The first fraction.
160
+ # @param [Fixnum] b The second fraction.
161
+ # @return [Fixnum] The result of the multiplication.
162
+ def int8_mult(a, b)
163
+ t = a * b + 0x80
164
+ ((t >> 8) + t) >> 8
165
+ end
166
+
167
+ # Composes two colors with an alpha channel using integer math.
168
+ #
169
+ # This version is faster than the version based on floating point math, so this
170
+ # compositing function is used by default.
171
+ #
172
+ # @param [Fixnum] fg The foreground color.
173
+ # @param [Fixnum] bg The foreground color.
174
+ # @return [Fixnum] The composited color.
175
+ # @see ChunkyPNG::Color#compose_precise
176
+ def compose_quick(fg, bg)
177
+ return fg if opaque?(fg)
178
+ return bg if fully_transparent?(fg)
179
+
180
+ a_com = int8_mult(0xff - a(fg), a(bg))
181
+ new_r = int8_mult(a(fg), r(fg)) + int8_mult(a_com, r(bg))
182
+ new_g = int8_mult(a(fg), g(fg)) + int8_mult(a_com, g(bg))
183
+ new_b = int8_mult(a(fg), b(fg)) + int8_mult(a_com, b(bg))
184
+ new_a = a(fg) + a_com
185
+ rgba(new_r, new_g, new_b, new_a)
186
+ end
187
+
188
+ # Composes two colors with an alpha channel using floating point math.
189
+ #
190
+ # This method uses more precise floating point math, but this precision is lost
191
+ # when the result is converted back to an integer. Because it is slower than
192
+ # the version based on integer math, that version is preferred.
193
+ #
194
+ # @param [Fixnum] fg The foreground color.
195
+ # @param [Fixnum] bg The foreground color.
196
+ # @return [Fixnum] The composited color.
197
+ # @see ChunkyPNG::Color#compose_quick
198
+ def compose_precise(fg, bg)
199
+ return fg if opaque?(fg)
200
+ return bg if fully_transparent?(fg)
201
+
202
+ fg_a = a(fg).to_f / MAX
203
+ bg_a = a(bg).to_f / MAX
204
+ a_com = (1.0 - fg_a) * bg_a
205
+
206
+ new_r = (fg_a * r(fg) + a_com * r(bg)).round
207
+ new_g = (fg_a * g(fg) + a_com * g(bg)).round
208
+ new_b = (fg_a * b(fg) + a_com * b(bg)).round
209
+ new_a = ((fg_a + a_com) * MAX).round
210
+ rgba(new_r, new_g, new_b, new_a)
211
+ end
212
+
213
+ alias :compose :compose_quick
214
+
215
+ # Blends the foreground and background color by taking the average of
216
+ # the components.
217
+ #
218
+ # @param [Fixnum] fg The foreground color.
219
+ # @param [Fixnum] bg The foreground color.
220
+ # @return [Fixnum] The blended color.
221
+ def blend(fg, bg)
222
+ (fg + bg) >> 1
223
+ end
224
+
225
+ ####################################################################
226
+ # CONVERSIONS
227
+ ####################################################################
228
+
229
+ # Returns a string representing this color using hex notation (i.e. #rrggbbaa).
230
+ #
231
+ # @param [Fixnum] value The color to convert.
232
+ # @return [String] The color in hex notation, starting with a pound sign.
233
+ def to_hex(color, include_alpha = true)
234
+ include_alpha ? ('#%08x' % color) : ('#%06x' % [color >> 8])
235
+ end
236
+
237
+ # Returns an array with the separate RGBA values for this color.
238
+ #
239
+ # @param [Fixnum] color The color to convert.
240
+ # @return [Array<Fixnum>] An array with 4 Fixnum elements.
241
+ def to_truecolor_alpha_bytes(color)
242
+ [r(color), g(color), b(color), a(color)]
243
+ end
244
+
245
+ # Returns an array with the separate RGB values for this color.
246
+ # The alpha channel will be discarded.
247
+ #
248
+ # @param [Fixnum] color The color to convert.
249
+ # @return [Array<Fixnum>] An array with 3 Fixnum elements.
250
+ def to_truecolor_bytes(color)
251
+ [r(color), g(color), b(color)]
252
+ end
253
+
254
+ # Returns an array with the grayscale teint value for this color.
255
+ #
256
+ # This method expects the r,g and b value to be equal, and the alpha
257
+ # channel will be discarded.
258
+ #
259
+ # @param [Fixnum] color The grayscale color to convert.
260
+ # @return [Array<Fixnum>] An array with 1 Fixnum element.
261
+ def to_grayscale_bytes(color)
262
+ [r(color)] # assumption r == g == b
263
+ end
264
+
265
+ # Returns an array with the grayscale teint and alpha channel values
266
+ # for this color.
267
+ #
268
+ # This method expects the r,g and b value to be equal.
269
+ #
270
+ # @param [Fixnum] color The grayscale color to convert.
271
+ # @return [Array<Fixnum>] An array with 2 Fixnum elements.
272
+ def to_grayscale_alpha_bytes(color)
273
+ [r(color), a(color)] # assumption r == g == b
274
+ end
275
+
276
+ ####################################################################
277
+ # COLOR CONSTANTS
278
+ ####################################################################
279
+
280
+ # Black pixel/color
281
+ BLACK = rgb( 0, 0, 0)
282
+
283
+ # White pixel/color
284
+ WHITE = rgb(255, 255, 255)
285
+
286
+ # Fully transparent pixel/color
287
+ TRANSPARENT = rgba(255, 255, 255, 0)
288
+
289
+ ####################################################################
290
+ # STATIC UTILITY METHODS
291
+ ####################################################################
292
+
293
+ # Returns the size in bytes of a pixel when it is stored using a given color mode.
294
+ # @param [Fixnum] color_mode The color mode in which the pixels are stored.
295
+ # @return [Fixnum] The number of bytes used per pixel in a datastream.
296
+ def bytesize(color_mode)
297
+ case color_mode
298
+ when ChunkyPNG::COLOR_INDEXED then 1
299
+ when ChunkyPNG::COLOR_TRUECOLOR then 3
300
+ when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then 4
301
+ when ChunkyPNG::COLOR_GRAYSCALE then 1
302
+ when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then 2
303
+ else raise "Don't know the bytesize of pixels in this colormode: #{color_mode}!"
304
+ end
305
+ end
306
+ end
307
+ end
@@ -1,71 +1,169 @@
1
1
  module ChunkyPNG
2
-
2
+
3
+ # The Datastream class represents a PNG formatted datastream. It supports
4
+ # both reading from and writing to strings, stremas and files.
5
+ #
6
+ # A PNG datastream begins with the PNG signature, and than contains multiple
7
+ # chunks, starting with a header (IHDR) chunk and finishing with an end
8
+ # (IEND) chunk.
9
+ #
10
+ # @see ChunkyPNG::Chunk
3
11
  class Datastream
4
-
12
+
13
+ # The signature that each PNG file or stream should begin with.
5
14
  SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack('C8')
6
-
15
+
16
+ # The header chunk of this datastream.
17
+ # @return [ChunkyPNG::Chunk::Header]
7
18
  attr_accessor :header_chunk
19
+
20
+ # All other chunks in this PNG file.
21
+ # @return [Array<ChunkyPNG::Chunk::Generic>]
8
22
  attr_accessor :other_chunks
23
+
24
+ # The chunk containing the image's palette.
25
+ # @return [ChunkyPNG::Chunk::Palette]
9
26
  attr_accessor :palette_chunk
27
+
28
+ # The chunk containing the transparency information of the palette.
29
+ # @return [ChunkyPNG::Chunk::Transparency]
10
30
  attr_accessor :transparency_chunk
31
+
32
+ # The chunks that together compose the images pixel data.
33
+ # @return [Array<ChunkyPNG::Chunk::ImageData>]
11
34
  attr_accessor :data_chunks
35
+
36
+ # The empty chunk that signals the end of this datastream
37
+ # @return [ChunkyPNG::Chunk::Header]
12
38
  attr_accessor :end_chunk
13
39
 
14
- def self.read(io)
15
- verify_signature!(io)
16
-
17
- ds = self.new
18
- until io.eof?
19
- chunk = ChunkyPNG::Chunk.read(io)
20
- case chunk
21
- when ChunkyPNG::Chunk::Header then ds.header_chunk = chunk
22
- when ChunkyPNG::Chunk::Palette then ds.palette_chunk = chunk
23
- when ChunkyPNG::Chunk::Transparency then ds.transparency_chunk = chunk
24
- when ChunkyPNG::Chunk::ImageData then ds.data_chunks << chunk
25
- when ChunkyPNG::Chunk::End then ds.end_chunk = chunk
26
- else ds.other_chunks << chunk
40
+
41
+ # Initializes a new Datastream instance.
42
+ def initialize
43
+ @other_chunks = []
44
+ @data_chunks = []
45
+ end
46
+
47
+ #############################################
48
+ # LOADING DATASTREAMS
49
+ #############################################
50
+
51
+ class << self
52
+
53
+ # Reads a PNG datastream from a string.
54
+ # @param [String] str The PNG encoded string to load from.
55
+ # @return [ChunkyPNG::Datastream] The loaded datastream instance.
56
+ def from_blob(str)
57
+ from_io(StringIO.new(str))
58
+ end
59
+
60
+ alias :from_string :from_blob
61
+
62
+ # Reads a PNG datastream from a file.
63
+ # @param [String] filename The path of the file to load from.
64
+ # @return [ChunkyPNG::Datastream] The loaded datastream instance.
65
+ def from_file(filename)
66
+ ds = nil
67
+ File.open(filename, 'rb') { |f| ds = from_io(f) }
68
+ ds
69
+ end
70
+
71
+ # Reads a PNG datastream from an input stream
72
+ # @param [IO] io The stream to read from.
73
+ # @return [ChunkyPNG::Datastream] The loaded datastream instance.
74
+ def from_io(io)
75
+ verify_signature!(io)
76
+
77
+ ds = self.new
78
+ until io.eof?
79
+ chunk = ChunkyPNG::Chunk.read(io)
80
+ case chunk
81
+ when ChunkyPNG::Chunk::Header then ds.header_chunk = chunk
82
+ when ChunkyPNG::Chunk::Palette then ds.palette_chunk = chunk
83
+ when ChunkyPNG::Chunk::Transparency then ds.transparency_chunk = chunk
84
+ when ChunkyPNG::Chunk::ImageData then ds.data_chunks << chunk
85
+ when ChunkyPNG::Chunk::End then ds.end_chunk = chunk
86
+ else ds.other_chunks << chunk
87
+ end
27
88
  end
89
+ return ds
90
+ end
91
+
92
+ # Verifies that the current stream is a PNG datastream by checking its signature.
93
+ #
94
+ # This method reads the PNG signature from the stream, setting the current position
95
+ # of the stream directly after the signature, where the IHDR chunk should begin.
96
+ #
97
+ # @raise [RuntimeError] An exception is raised if the PNG signature is not found at
98
+ # the beginning of the stream.
99
+ def verify_signature!(io)
100
+ signature = io.read(ChunkyPNG::Datastream::SIGNATURE.length)
101
+ raise "PNG signature not found!" unless signature == ChunkyPNG::Datastream::SIGNATURE
28
102
  end
29
- return ds
30
103
  end
31
104
 
32
- def self.verify_signature!(io)
33
- signature = io.read(ChunkyPNG::Datastream::SIGNATURE.length)
34
- raise "PNG signature not found!" unless signature == ChunkyPNG::Datastream::SIGNATURE
105
+ #############################################
106
+ # CHUNKS
107
+ #############################################
108
+
109
+ # Enumerates the chunks in this datastream.
110
+ #
111
+ # This will iterate over the chunks using the order in which the chunks
112
+ # should appear in the PNG file.
113
+ #
114
+ # @yield [ChunkyPNG::Chunk::Base] The chunks in this datastrean, one by one.
115
+ # @see ChunkyPNG::Datastream#chunks
116
+ def each_chunk
117
+ yield(header_chunk)
118
+ other_chunks.each { |chunk| yield(chunk) }
119
+ yield(palette_chunk) if palette_chunk
120
+ yield(transparency_chunk) if transparency_chunk
121
+ data_chunks.each { |chunk| yield(chunk) }
122
+ yield(end_chunk)
35
123
  end
36
-
124
+
125
+ # Returns an enumerator instance for this datastream's chunks.
126
+ # @return [Enumerable::Enumerator] An enumerator for the :each_chunk method.
127
+ # @see ChunkyPNG::Datastream#each_chunk
37
128
  def chunks
38
- cs = [header_chunk]
39
- cs += other_chunks
40
- cs << palette_chunk if palette_chunk
41
- cs << transparency_chunk if transparency_chunk
42
- cs += data_chunks
43
- cs << end_chunk
44
- return cs
129
+ enum_for(:each_chunk)
45
130
  end
46
131
 
47
- def initialize
48
- @other_chunks = []
49
- @data_chunks = []
132
+ # Returns all the textual metadata key/value pairs as hash.
133
+ def metadata
134
+ metadata = {}
135
+ other_chunks.select do |chunk|
136
+ metadata[chunk.keyword] = chunk.value if chunk.respond_to?(:keyword)
137
+ end
138
+ metadata
50
139
  end
51
-
140
+
141
+ #############################################
142
+ # WRITING DATASTREAMS
143
+ #############################################
144
+
145
+ # Writes the datastream to the given output stream.
146
+ # @param [IO] io The output stream to write to.
52
147
  def write(io)
53
148
  io << SIGNATURE
54
- chunks.each { |c| c.write(io) }
55
- end
56
-
57
- def idat_chunks(data)
58
- streamdata = Zlib::Deflate.deflate(data)
59
- # TODO: Split long streamdata over multiple chunks
60
- return [ ChunkyPNG::Chunk::ImageData.new('IDAT', streamdata) ]
149
+ each_chunk { |c| c.write(io) }
61
150
  end
62
-
63
- def pixel_matrix=(pixel_matrix)
64
- @pixel_matrix = pixel_matrix
151
+
152
+ # Saves this datastream as a PNG file.
153
+ # @param [String] filename The filename to use.
154
+ def save(filename)
155
+ File.open(filename, 'wb') { |f| write(f) }
65
156
  end
66
-
67
- def pixel_matrix
68
- @pixel_matrix ||= ChunkyPNG::PixelMatrix.decode(self)
157
+
158
+ # Encodes this datastream into a string.
159
+ # @return [String] The encoded PNG datastream.
160
+ def to_blob
161
+ str = StringIO.new
162
+ write(str)
163
+ return str.string
69
164
  end
165
+
166
+ alias :to_string :to_blob
167
+ alias :to_s :to_blob
70
168
  end
71
169
  end