chunky_png 0.0.5 → 0.5.0

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 (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