chunky_png 1.3.8 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ruby.yml +35 -0
  3. data/.standard.yml +16 -0
  4. data/.yardopts +1 -1
  5. data/CHANGELOG.rdoc +16 -4
  6. data/CONTRIBUTING.rdoc +17 -8
  7. data/Gemfile +12 -4
  8. data/LICENSE +1 -1
  9. data/README.md +15 -9
  10. data/Rakefile +5 -3
  11. data/benchmarks/decoding_benchmark.rb +17 -17
  12. data/benchmarks/encoding_benchmark.rb +22 -19
  13. data/benchmarks/filesize_benchmark.rb +6 -6
  14. data/bin/rake +29 -0
  15. data/bin/standardrb +29 -0
  16. data/chunky_png.gemspec +21 -13
  17. data/docs/.gitignore +3 -0
  18. data/docs/CNAME +1 -0
  19. data/docs/_config.yml +9 -0
  20. data/docs/_posts/2010-01-14-memory-efficiency-when-using-ruby.md +136 -0
  21. data/docs/_posts/2010-01-17-ode-to-array-pack-and-string-unpack.md +82 -0
  22. data/docs/_posts/2014-11-07-the-value-of-a-pure-ruby-library.md +61 -0
  23. data/docs/index.md +88 -0
  24. data/lib/chunky_png/canvas/adam7_interlacing.rb +16 -10
  25. data/lib/chunky_png/canvas/data_url_exporting.rb +3 -3
  26. data/lib/chunky_png/canvas/data_url_importing.rb +3 -3
  27. data/lib/chunky_png/canvas/drawing.rb +30 -43
  28. data/lib/chunky_png/canvas/masking.rb +14 -14
  29. data/lib/chunky_png/canvas/operations.rb +28 -24
  30. data/lib/chunky_png/canvas/png_decoding.rb +39 -33
  31. data/lib/chunky_png/canvas/png_encoding.rb +111 -103
  32. data/lib/chunky_png/canvas/resampling.rb +27 -32
  33. data/lib/chunky_png/canvas/stream_exporting.rb +8 -8
  34. data/lib/chunky_png/canvas/stream_importing.rb +8 -8
  35. data/lib/chunky_png/canvas.rb +31 -28
  36. data/lib/chunky_png/chunk.rb +142 -69
  37. data/lib/chunky_png/color.rb +218 -212
  38. data/lib/chunky_png/datastream.rb +24 -30
  39. data/lib/chunky_png/dimension.rb +18 -11
  40. data/lib/chunky_png/image.rb +11 -11
  41. data/lib/chunky_png/palette.rb +13 -14
  42. data/lib/chunky_png/point.rb +27 -26
  43. data/lib/chunky_png/rmagick.rb +10 -10
  44. data/lib/chunky_png/vector.rb +28 -29
  45. data/lib/chunky_png/version.rb +3 -1
  46. data/lib/chunky_png.rb +46 -45
  47. data/spec/chunky_png/canvas/adam7_interlacing_spec.rb +20 -21
  48. data/spec/chunky_png/canvas/data_url_exporting_spec.rb +8 -5
  49. data/spec/chunky_png/canvas/data_url_importing_spec.rb +5 -6
  50. data/spec/chunky_png/canvas/drawing_spec.rb +46 -38
  51. data/spec/chunky_png/canvas/masking_spec.rb +15 -16
  52. data/spec/chunky_png/canvas/operations_spec.rb +68 -67
  53. data/spec/chunky_png/canvas/png_decoding_spec.rb +37 -38
  54. data/spec/chunky_png/canvas/png_encoding_spec.rb +59 -50
  55. data/spec/chunky_png/canvas/resampling_spec.rb +19 -21
  56. data/spec/chunky_png/canvas/stream_exporting_spec.rb +47 -27
  57. data/spec/chunky_png/canvas/stream_importing_spec.rb +10 -11
  58. data/spec/chunky_png/canvas_spec.rb +63 -52
  59. data/spec/chunky_png/color_spec.rb +115 -114
  60. data/spec/chunky_png/datastream_spec.rb +98 -19
  61. data/spec/chunky_png/dimension_spec.rb +10 -10
  62. data/spec/chunky_png/image_spec.rb +11 -14
  63. data/spec/chunky_png/point_spec.rb +21 -23
  64. data/spec/chunky_png/rmagick_spec.rb +7 -8
  65. data/spec/chunky_png/vector_spec.rb +21 -17
  66. data/spec/chunky_png_spec.rb +2 -2
  67. data/spec/png_suite_spec.rb +35 -40
  68. data/spec/resources/itxt_chunk.png +0 -0
  69. data/spec/spec_helper.rb +15 -9
  70. data/tasks/benchmarks.rake +7 -8
  71. metadata +65 -25
  72. data/.travis.yml +0 -16
  73. data/lib/chunky_png/compatibility.rb +0 -15
@@ -1,9 +1,9 @@
1
+ # frozen-string-literal: true
2
+
1
3
  module ChunkyPNG
2
4
  class Canvas
3
-
4
5
  # Methods to save load a canvas from to stream, encoded in RGB, RGBA, BGR or ABGR format.
5
6
  module StreamExporting
6
-
7
7
  # Creates an RGB-formatted pixelstream with the pixel data from this canvas.
8
8
  #
9
9
  # Note that this format is fast but bloated, because no compression is used
@@ -12,7 +12,7 @@ module ChunkyPNG
12
12
  #
13
13
  # @return [String] The RGBA-formatted pixel data.
14
14
  def to_rgba_stream
15
- pixels.pack('N*')
15
+ pixels.pack("N*")
16
16
  end
17
17
 
18
18
  # Creates an RGB-formatted pixelstream with the pixel data from this canvas.
@@ -23,14 +23,14 @@ module ChunkyPNG
23
23
  #
24
24
  # @return [String] The RGB-formatted pixel data.
25
25
  def to_rgb_stream
26
- pixels.pack('NX' * pixels.length)
26
+ pixels.pack("NX" * pixels.length)
27
27
  end
28
-
28
+
29
29
  # Creates a stream of the alpha channel of this canvas.
30
30
  #
31
31
  # @return [String] The 0-255 alpha values of all pixels packed as string
32
32
  def to_alpha_channel_stream
33
- pixels.pack('C*')
33
+ pixels.pack("C*")
34
34
  end
35
35
 
36
36
  # Creates a grayscale stream of this canvas.
@@ -40,7 +40,7 @@ module ChunkyPNG
40
40
  #
41
41
  # @return [String] The 0-255 grayscale values of all pixels packed as string.
42
42
  def to_grayscale_stream
43
- pixels.pack('nX' * pixels.length)
43
+ pixels.pack("nX" * pixels.length)
44
44
  end
45
45
 
46
46
  # Creates an ABGR-formatted pixelstream with the pixel data from this canvas.
@@ -51,7 +51,7 @@ module ChunkyPNG
51
51
  #
52
52
  # @return [String] The RGBA-formatted pixel data.
53
53
  def to_abgr_stream
54
- pixels.pack('V*')
54
+ pixels.pack("V*")
55
55
  end
56
56
  end
57
57
  end
@@ -1,9 +1,9 @@
1
+ # frozen-string-literal: true
2
+
1
3
  module ChunkyPNG
2
4
  class Canvas
3
-
4
5
  # Methods to quickly load a canvas from a stream, encoded in RGB, RGBA, BGR or ABGR format.
5
6
  module StreamImporting
6
-
7
7
  # Creates a canvas by reading pixels from an RGB formatted stream with a
8
8
  # provided with and height.
9
9
  #
@@ -18,9 +18,9 @@ module ChunkyPNG
18
18
  def from_rgb_stream(width, height, stream)
19
19
  string = stream.respond_to?(:read) ? stream.read(3 * width * height) : stream.to_s[0, 3 * width * height]
20
20
  string << ChunkyPNG::EXTRA_BYTE # Add a fourth byte to the last RGB triple.
21
- unpacker = 'NX' * (width * height)
21
+ unpacker = "NX" * (width * height)
22
22
  pixels = string.unpack(unpacker).map { |color| color | 0x000000ff }
23
- self.new(width, height, pixels)
23
+ new(width, height, pixels)
24
24
  end
25
25
 
26
26
  # Creates a canvas by reading pixels from an RGBA formatted stream with a
@@ -36,7 +36,7 @@ module ChunkyPNG
36
36
  # @return [ChunkyPNG::Canvas] The newly constructed canvas instance.
37
37
  def from_rgba_stream(width, height, stream)
38
38
  string = stream.respond_to?(:read) ? stream.read(4 * width * height) : stream.to_s[0, 4 * width * height]
39
- self.new(width, height, string.unpack("N*"))
39
+ new(width, height, string.unpack("N*"))
40
40
  end
41
41
 
42
42
  # Creates a canvas by reading pixels from an BGR formatted stream with a
@@ -53,8 +53,8 @@ module ChunkyPNG
53
53
  def from_bgr_stream(width, height, stream)
54
54
  string = ChunkyPNG::EXTRA_BYTE.dup # Add a first byte to the first BGR triple.
55
55
  string << (stream.respond_to?(:read) ? stream.read(3 * width * height) : stream.to_s[0, 3 * width * height])
56
- pixels = string.unpack("@1" << ('XV' * (width * height))).map { |color| color | 0x000000ff }
57
- self.new(width, height, pixels)
56
+ pixels = string.unpack("@1#{"XV" * (width * height)}").map { |color| color | 0x000000ff }
57
+ new(width, height, pixels)
58
58
  end
59
59
 
60
60
  # Creates a canvas by reading pixels from an ARGB formatted stream with a
@@ -70,7 +70,7 @@ module ChunkyPNG
70
70
  # @return [ChunkyPNG::Canvas] The newly constructed canvas instance.
71
71
  def from_abgr_stream(width, height, stream)
72
72
  string = stream.respond_to?(:read) ? stream.read(4 * width * height) : stream.to_s[0, 4 * width * height]
73
- self.new(width, height, string.unpack("V*"))
73
+ new(width, height, string.unpack("V*"))
74
74
  end
75
75
  end
76
76
  end
@@ -1,14 +1,16 @@
1
- require 'chunky_png/canvas/png_encoding'
2
- require 'chunky_png/canvas/png_decoding'
3
- require 'chunky_png/canvas/adam7_interlacing'
4
- require 'chunky_png/canvas/stream_exporting'
5
- require 'chunky_png/canvas/stream_importing'
6
- require 'chunky_png/canvas/data_url_exporting'
7
- require 'chunky_png/canvas/data_url_importing'
8
- require 'chunky_png/canvas/operations'
9
- require 'chunky_png/canvas/drawing'
10
- require 'chunky_png/canvas/resampling'
11
- require 'chunky_png/canvas/masking'
1
+ # frozen-string-literal: true
2
+
3
+ require "chunky_png/canvas/png_encoding"
4
+ require "chunky_png/canvas/png_decoding"
5
+ require "chunky_png/canvas/adam7_interlacing"
6
+ require "chunky_png/canvas/stream_exporting"
7
+ require "chunky_png/canvas/stream_importing"
8
+ require "chunky_png/canvas/data_url_exporting"
9
+ require "chunky_png/canvas/data_url_importing"
10
+ require "chunky_png/canvas/operations"
11
+ require "chunky_png/canvas/drawing"
12
+ require "chunky_png/canvas/resampling"
13
+ require "chunky_png/canvas/masking"
12
14
 
13
15
  module ChunkyPNG
14
16
  # The ChunkyPNG::Canvas class represents a raster image as a matrix of
@@ -56,7 +58,6 @@ module ChunkyPNG
56
58
  # This array always should have +width * height+ elements.
57
59
  attr_reader :pixels
58
60
 
59
-
60
61
  #################################################################
61
62
  # CONSTRUCTORS
62
63
  #################################################################
@@ -68,7 +69,7 @@ module ChunkyPNG
68
69
  # @param [Integer] height The height in pixels of this canvas
69
70
  # @param [Integer, ...] background_color The initial background color of
70
71
  # this canvas. This can be a color value or any value that
71
- # {ChunkyPNG::Color.parse} can handle.
72
+ # {ChunkyPNG::Color#parse} can handle.
72
73
  #
73
74
  # @overload initialize(width, height, initial)
74
75
  # @param [Integer] width The width in pixels of this canvas
@@ -78,9 +79,10 @@ module ChunkyPNG
78
79
  def initialize(width, height, initial = ChunkyPNG::Color::TRANSPARENT)
79
80
  @width, @height = width, height
80
81
 
81
- if initial.kind_of?(Array)
82
- unless initial.length == width * height
83
- raise ArgumentError, "The initial array should have #{width}x#{height} = #{width*height} elements!"
82
+ if initial.is_a?(Array)
83
+ pixel_count = width * height
84
+ unless initial.length == pixel_count
85
+ raise ArgumentError, "The initial array should have #{width}x#{height} = #{pixel_count} elements!"
84
86
  end
85
87
  @pixels = initial
86
88
  else
@@ -104,7 +106,6 @@ module ChunkyPNG
104
106
  new(canvas.width, canvas.height, canvas.pixels.dup)
105
107
  end
106
108
 
107
-
108
109
  #################################################################
109
110
  # PROPERTIES
110
111
  #################################################################
@@ -143,7 +144,7 @@ module ChunkyPNG
143
144
  #
144
145
  # @param [Integer] x The x-coordinate of the pixel (column)
145
146
  # @param [Integer] y The y-coordinate of the pixel (row)
146
- # @param [Integer] pixel The new color for the provided coordinates.
147
+ # @param [Integer] color The new color for the provided coordinates.
147
148
  # @return [Integer] The new color value for this pixel, i.e.
148
149
  # <tt>color</tt>.
149
150
  def set_pixel(x, y, color)
@@ -155,7 +156,7 @@ module ChunkyPNG
155
156
  #
156
157
  # @param [Integer] x The x-coordinate of the pixel (column)
157
158
  # @param [Integer] y The y-coordinate of the pixel (row)
158
- # @param [Integer] pixel The new color value for the provided coordinates.
159
+ # @param [Integer] color The new color value for the provided coordinates.
159
160
  # @return [Integer] The new color value for this pixel, i.e.
160
161
  # <tt>color</tt>, or <tt>nil</tt> if the coordinates are out of bounds.
161
162
  def set_pixel_if_within_bounds(x, y, color)
@@ -233,7 +234,7 @@ module ChunkyPNG
233
234
  dimension.include?(ChunkyPNG::Point(*point_like))
234
235
  end
235
236
 
236
- alias_method :include?, :include_point?
237
+ alias include? include_point?
237
238
 
238
239
  # Checks whether the given x- and y-coordinate are in the range of the
239
240
  # canvas
@@ -274,11 +275,13 @@ module ChunkyPNG
274
275
  # @return [true, false] True if the size and pixel values of the other
275
276
  # canvas are exactly the same as this canvas's size and pixel values.
276
277
  def eql?(other)
277
- other.kind_of?(self.class) && other.pixels == self.pixels &&
278
- other.width == self.width && other.height == self.height
278
+ other.is_a?(self.class) &&
279
+ other.pixels == pixels &&
280
+ other.width == width &&
281
+ other.height == height
279
282
  end
280
283
 
281
- alias :== :eql?
284
+ alias == eql?
282
285
 
283
286
  #################################################################
284
287
  # EXPORTING
@@ -294,9 +297,9 @@ module ChunkyPNG
294
297
  # @return [String] A nicely formatted string representation of this canvas.
295
298
  # @private
296
299
  def inspect
297
- inspected = "<#{self.class.name} #{width}x#{height} ["
300
+ inspected = +"<#{self.class.name} #{width}x#{height} ["
298
301
  for y in 0...height
299
- inspected << "\n\t[" << row(y).map { |p| ChunkyPNG::Color.to_hex(p) }.join(' ') << ']'
302
+ inspected << "\n\t[" << row(y).map { |p| ChunkyPNG::Color.to_hex(p) }.join(" ") << "]"
300
303
  end
301
304
  inspected << "\n]>"
302
305
  end
@@ -358,13 +361,13 @@ module ChunkyPNG
358
361
 
359
362
  # Throws an exception if the matrix width and height does not match this canvas' dimensions.
360
363
  def assert_size!(matrix_width, matrix_height)
361
- if width != matrix_width
364
+ if width != matrix_width
362
365
  raise ChunkyPNG::ExpectationFailed,
363
- 'The width of the matrix does not match the canvas width!'
366
+ "The width of the matrix does not match the canvas width!"
364
367
  end
365
368
  if height != matrix_height
366
369
  raise ChunkyPNG::ExpectationFailed,
367
- 'The height of the matrix does not match the canvas height!'
370
+ "The height of the matrix does not match the canvas height!"
368
371
  end
369
372
  true
370
373
  end
@@ -1,3 +1,5 @@
1
+ # frozen-string-literal: true
2
+
1
3
  module ChunkyPNG
2
4
  # A PNG datastream consists of multiple chunks. This module, and the classes
3
5
  # contained within, help with handling these chunks. It supports both reading
@@ -16,10 +18,10 @@ module ChunkyPNG
16
18
  # @param io [IO, #read] The IO stream to read from.
17
19
  # @return [ChunkyPNG::Chung::Base] The loaded chunk instance.
18
20
  def self.read(io)
19
- length, type = read_bytes(io, 8).unpack('Na4')
21
+ length, type = read_bytes(io, 8).unpack("Na4")
20
22
 
21
23
  content = read_bytes(io, length)
22
- crc = read_bytes(io, 4).unpack('N').first
24
+ crc = read_bytes(io, 4).unpack("N").first
23
25
  verify_crc!(type, content, crc)
24
26
 
25
27
  CHUNK_TYPES.fetch(type, Generic).read(type, content)
@@ -74,8 +76,8 @@ module ChunkyPNG
74
76
  # @param io [IO] The IO stream to write to.
75
77
  # @param content [String] The content for this chunk.
76
78
  def write_with_crc(io, content)
77
- io << [content.length].pack('N') << type << content
78
- io << [Zlib.crc32(content, Zlib.crc32(type))].pack('N')
79
+ io << [content.length].pack("N") << type << content
80
+ io << [Zlib.crc32(content, Zlib.crc32(type))].pack("N")
79
81
  end
80
82
 
81
83
  # Writes the chunk to the IO stream.
@@ -84,7 +86,7 @@ module ChunkyPNG
84
86
  # and will calculate and append the checksum automatically.
85
87
  # @param io [IO] The IO stream to write to.
86
88
  def write(io)
87
- write_with_crc(io, content || '')
89
+ write_with_crc(io, content || "")
88
90
  end
89
91
  end
90
92
 
@@ -95,8 +97,8 @@ module ChunkyPNG
95
97
  # written by the +write+ method.
96
98
  attr_accessor :content
97
99
 
98
- def initialize(type, content = '')
99
- super(type, :content => content)
100
+ def initialize(type, content = "")
101
+ super(type, content: content)
100
102
  end
101
103
 
102
104
  # Creates an instance, given the chunk's type and content.
@@ -117,12 +119,13 @@ module ChunkyPNG
117
119
  # PNG spec, except for color depth: Only 8-bit depth images are supported.
118
120
  # Note that it is still possible to access the chunk for such an image, but
119
121
  # ChunkyPNG will raise an exception if you try to access the pixel data.
122
+ #
123
+ # @see https://www.w3.org/TR/PNG/#11IHDR
120
124
  class Header < Base
121
- attr_accessor :width, :height, :depth, :color, :compression, :filtering,
122
- :interlace
125
+ attr_accessor :width, :height, :depth, :color, :compression, :filtering, :interlace
123
126
 
124
127
  def initialize(attrs = {})
125
- super('IHDR', attrs)
128
+ super("IHDR", attrs)
126
129
  @depth ||= 8
127
130
  @color ||= ChunkyPNG::COLOR_TRUECOLOR
128
131
  @compression ||= ChunkyPNG::COMPRESSION_DEFAULT
@@ -137,31 +140,41 @@ module ChunkyPNG
137
140
  # @return [ChunkyPNG::Chunk::End] The new Header chunk instance with the
138
141
  # variables set to the values according to the content.
139
142
  def self.read(type, content)
140
- fields = content.unpack('NNC5')
141
- new(:width => fields[0],
142
- :height => fields[1],
143
- :depth => fields[2],
144
- :color => fields[3],
145
- :compression => fields[4],
146
- :filtering => fields[5],
147
- :interlace => fields[6])
143
+ fields = content.unpack("NNC5")
144
+ new(
145
+ width: fields[0],
146
+ height: fields[1],
147
+ depth: fields[2],
148
+ color: fields[3],
149
+ compression: fields[4],
150
+ filtering: fields[5],
151
+ interlace: fields[6]
152
+ )
148
153
  end
149
154
 
150
155
  # Returns the content for this chunk when it gets written to a file, by
151
156
  # packing the image information variables into the correct format.
152
157
  # @return [String] The 13-byte content for the header chunk.
153
158
  def content
154
- [width, height, depth, color, compression, filtering, interlace].
155
- pack('NNC5')
159
+ [
160
+ width,
161
+ height,
162
+ depth,
163
+ color,
164
+ compression,
165
+ filtering,
166
+ interlace,
167
+ ].pack("NNC5")
156
168
  end
157
169
  end
158
170
 
159
171
  # The End (IEND) chunk indicates the last chunk of a PNG stream. It does
160
172
  # not contain any data.
173
+ #
174
+ # @see https://www.w3.org/TR/PNG/#11IEND
161
175
  class End < Base
162
-
163
176
  def initialize
164
- super('IEND')
177
+ super("IEND")
165
178
  end
166
179
 
167
180
  # Reads the END chunk. It will check if the content is empty.
@@ -170,22 +183,23 @@ module ChunkyPNG
170
183
  # @param content [String] The content read from the chunk. Should be
171
184
  # empty.
172
185
  # @return [ChunkyPNG::Chunk::End] The new End chunk instance.
173
- # @raise [RuntimeError] Raises an exception if the content was not empty.
186
+ # @raise [ChunkyPNG::ExpectationFailed] Raises an exception if the content was not empty.
174
187
  def self.read(type, content)
175
- raise ExpectationFailed, 'The IEND chunk should be empty!' if content.bytesize > 0
176
- self.new
188
+ raise ExpectationFailed, "The IEND chunk should be empty!" if content.bytesize > 0
189
+ new
177
190
  end
178
191
 
179
192
  # Returns an empty string, because this chunk should always be empty.
180
193
  # @return [""] An empty string.
181
194
  def content
182
- ChunkyPNG::Datastream.empty_bytearray
195
+ "".b
183
196
  end
184
197
  end
185
198
 
186
199
  # The Palette (PLTE) chunk contains the image's palette, i.e. the
187
200
  # 8-bit RGB colors this image is using.
188
201
  #
202
+ # @see https://www.w3.org/TR/PNG/#11PLTE
189
203
  # @see ChunkyPNG::Chunk::Transparency
190
204
  # @see ChunkyPNG::Palette
191
205
  class Palette < Generic
@@ -203,6 +217,7 @@ module ChunkyPNG
203
217
  # Images having a color mode that already includes an alpha channel, this
204
218
  # chunk should not be included.
205
219
  #
220
+ # @see https://www.w3.org/TR/PNG/#11tRNS
206
221
  # @see ChunkyPNG::Chunk::Palette
207
222
  # @see ChunkyPNG::Palette
208
223
  class Transparency < Generic
@@ -214,7 +229,7 @@ module ChunkyPNG
214
229
  # @return [Array<Integer>] Returns an array of alpha channel values
215
230
  # [0-255].
216
231
  def palette_alpha_channel
217
- content.unpack('C*')
232
+ content.unpack("C*")
218
233
  end
219
234
 
220
235
  # Returns the truecolor entry to be replaced by transparent pixels,
@@ -224,9 +239,8 @@ module ChunkyPNG
224
239
  #
225
240
  # @return [Integer] The color to replace with fully transparent pixels.
226
241
  def truecolor_entry(bit_depth)
227
- values = content.unpack('nnn').map do |c|
228
- ChunkyPNG::Canvas.send(:"decode_png_resample_#{bit_depth}bit_value", c)
229
- end
242
+ decode_method_name = :"decode_png_resample_#{bit_depth}bit_value"
243
+ values = content.unpack("nnn").map { |c| ChunkyPNG::Canvas.send(decode_method_name, c) }
230
244
  ChunkyPNG::Color.rgb(*values)
231
245
  end
232
246
 
@@ -238,12 +252,23 @@ module ChunkyPNG
238
252
  # @return [Integer] The (grayscale) color to replace with fully
239
253
  # transparent pixels.
240
254
  def grayscale_entry(bit_depth)
241
- value = ChunkyPNG::Canvas.send(:"decode_png_resample_#{bit_depth}bit_value", content.unpack('n')[0])
255
+ value = ChunkyPNG::Canvas.send(:"decode_png_resample_#{bit_depth}bit_value", content.unpack("n")[0])
242
256
  ChunkyPNG::Color.grayscale(value)
243
257
  end
244
258
  end
245
259
 
260
+ # An image data (IDAT) chunk holds (part of) the compressed image pixel data.
261
+ #
262
+ # The data of an image can be split over multiple chunks, which will have to be combined
263
+ # and inflated in order to decode an image. See {{.combine_chunks}} to combine chunks
264
+ # to decode, and {{.split_in_chunks}} for encoding a pixeldata stream into IDAT chunks.
265
+ #
266
+ # @see https://www.w3.org/TR/PNG/#11IDAT
246
267
  class ImageData < Generic
268
+ # Combines the list of IDAT chunks and inflates their contents to produce the
269
+ # pixeldata stream for the image.
270
+ #
271
+ # @return [String] The combined, inflated pixeldata as binary string
247
272
  def self.combine_chunks(data_chunks)
248
273
  zstream = Zlib::Inflate.new
249
274
  data_chunks.each { |c| zstream << c.content }
@@ -252,31 +277,38 @@ module ChunkyPNG
252
277
  inflated
253
278
  end
254
279
 
280
+ # Splits and compresses a pixeldata stream into a list of IDAT chunks.
281
+ #
282
+ # @param data [String] The binary string of pixeldata
283
+ # @param level [Integer] The compression level to use.
284
+ # @param chunk_size [Integer] The maximum size of a chunk.
285
+ # @return Array<ChunkyPNG::Chunk::ImageData> The list of IDAT chunks.
255
286
  def self.split_in_chunks(data, level = Zlib::DEFAULT_COMPRESSION, chunk_size = 2147483647)
256
287
  streamdata = Zlib::Deflate.deflate(data, level)
257
288
  # TODO: Split long streamdata over multiple chunks
258
- [ ChunkyPNG::Chunk::ImageData.new('IDAT', streamdata) ]
289
+ [ChunkyPNG::Chunk::ImageData.new("IDAT", streamdata)]
259
290
  end
260
291
  end
261
292
 
262
293
  # The Text (tEXt) chunk contains keyword/value metadata about the PNG
263
- # stream. In this chunk, the value is stored uncompressed.
294
+ # stream. In this chunk, the value is stored uncompressed.
264
295
  #
265
296
  # The tEXt chunk only supports Latin-1 encoded textual data. If you need
266
297
  # UTF-8 support, check out the InternationalText chunk type.
267
298
  #
299
+ # @see https://www.w3.org/TR/PNG/#11tEXt
268
300
  # @see ChunkyPNG::Chunk::CompressedText
269
301
  # @see ChunkyPNG::Chunk::InternationalText
270
302
  class Text < Base
271
303
  attr_accessor :keyword, :value
272
304
 
273
305
  def initialize(keyword, value)
274
- super('tEXt')
306
+ super("tEXt")
275
307
  @keyword, @value = keyword, value
276
308
  end
277
309
 
278
310
  def self.read(type, content)
279
- keyword, value = content.unpack('Z*a*')
311
+ keyword, value = content.unpack("Z*a*")
280
312
  new(keyword, value)
281
313
  end
282
314
 
@@ -285,7 +317,7 @@ module ChunkyPNG
285
317
  #
286
318
  # @return The content that should be written to the datastream.
287
319
  def content
288
- [keyword, value].pack('Z*a*')
320
+ [keyword, value].pack("Z*a*")
289
321
  end
290
322
  end
291
323
 
@@ -293,18 +325,19 @@ module ChunkyPNG
293
325
  # PNG stream. In this chunk, the value is compressed using Deflate
294
326
  # compression.
295
327
  #
328
+ # @see https://www.w3.org/TR/PNG/#11zTXt
296
329
  # @see ChunkyPNG::Chunk::CompressedText
297
330
  # @see ChunkyPNG::Chunk::InternationalText
298
331
  class CompressedText < Base
299
332
  attr_accessor :keyword, :value
300
333
 
301
334
  def initialize(keyword, value)
302
- super('zTXt')
335
+ super("zTXt")
303
336
  @keyword, @value = keyword, value
304
337
  end
305
338
 
306
339
  def self.read(type, content)
307
- keyword, compression, value = content.unpack('Z*Ca*')
340
+ keyword, compression, value = content.unpack("Z*Ca*")
308
341
  raise ChunkyPNG::NotSupported, "Compression method #{compression.inspect} not supported!" unless compression == ChunkyPNG::COMPRESSION_DEFAULT
309
342
  new(keyword, Zlib::Inflate.inflate(value))
310
343
  end
@@ -314,67 +347,107 @@ module ChunkyPNG
314
347
  #
315
348
  # @return The content that should be written to the datastream.
316
349
  def content
317
- [keyword, ChunkyPNG::COMPRESSION_DEFAULT, Zlib::Deflate.deflate(value)].
318
- pack('Z*Ca*')
350
+ [
351
+ keyword,
352
+ ChunkyPNG::COMPRESSION_DEFAULT,
353
+ Zlib::Deflate.deflate(value),
354
+ ].pack("Z*Ca*")
319
355
  end
320
356
  end
321
357
 
322
358
  # The Physical (pHYs) chunk specifies the intended pixel size or aspect
323
359
  # ratio for display of the image.
324
360
  #
325
- # http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.pHYs
326
- #
327
- class Physical < Generic
361
+ # @see https://www.w3.org/TR/PNG/#11pHYs
362
+ class Physical < Base
328
363
  attr_accessor :ppux, :ppuy, :unit
329
364
 
330
365
  def initialize(ppux, ppuy, unit = :unknown)
331
- raise ArgumentError, 'unit must be either :meters or :unknown' unless [:meters, :unknown].member?(unit)
332
- super('pHYs')
366
+ raise ArgumentError, "unit must be either :meters or :unknown" unless [:meters, :unknown].member?(unit)
367
+ super("pHYs")
333
368
  @ppux, @ppuy, @unit = ppux, ppuy, unit
334
369
  end
335
370
 
336
371
  def dpix
337
- raise ChunkyPNG::UnitsUnknown, 'the PNG specifies its physical aspect ratio, but does not specify the units of its pixels\' physical dimensions' unless unit == :meters
372
+ raise ChunkyPNG::UnitsUnknown, "the PNG specifies its physical aspect ratio, but does not specify the units of its pixels' physical dimensions" unless unit == :meters
338
373
  ppux * INCHES_PER_METER
339
374
  end
340
375
 
341
376
  def dpiy
342
- raise ChunkyPNG::UnitsUnknown, 'the PNG specifies its physical aspect ratio, but does not specify the units of its pixels\' physical dimensions' unless unit == :meters
377
+ raise ChunkyPNG::UnitsUnknown, "the PNG specifies its physical aspect ratio, but does not specify the units of its pixels' physical dimensions" unless unit == :meters
343
378
  ppuy * INCHES_PER_METER
344
379
  end
345
380
 
346
381
  def self.read(type, content)
347
- ppux, ppuy, unit = content.unpack('NNC')
382
+ ppux, ppuy, unit = content.unpack("NNC")
348
383
  unit = unit == 1 ? :meters : :unknown
349
384
  new(ppux, ppuy, unit)
350
385
  end
351
386
 
352
- # Creates the content to write to the stream, by concatenating the
353
- # keyword with the deflated value, joined by a null character.
354
- #
355
- # @return The content that should be written to the datastream.
387
+ # Assembles the content to write to the stream for this chunk.
388
+ # @return [String] The binary content that should be written to the datastream.
356
389
  def content
357
- [ppux, ppuy, unit == :meters ? 1 : 0].pack('NNC')
390
+ [ppux, ppuy, unit == :meters ? 1 : 0].pack("NNC")
358
391
  end
359
392
 
360
393
  INCHES_PER_METER = 0.0254
361
394
  end
362
395
 
363
- # The Text (iTXt) chunk contains keyword/value metadata about the PNG
364
- # stream.
396
+ # The InternationalText (iTXt) chunk contains keyword/value metadata about the PNG
397
+ # stream, translated to a given locale.
365
398
  #
366
399
  # The metadata in this chunk can be encoded using UTF-8 characters.
367
400
  # Moreover, it is possible to define the language of the metadata, and give
368
401
  # a translation of the keyword name. Finally, it supports bot compressed
369
402
  # and uncompressed values.
370
403
  #
371
- # @todo This chunk is currently not implemented, but merely read and
372
- # written back intact.
373
- #
404
+ # @see https://www.w3.org/TR/PNG/#11iTXt
374
405
  # @see ChunkyPNG::Chunk::Text
375
406
  # @see ChunkyPNG::Chunk::CompressedText
376
- class InternationalText < Generic
377
- # TODO
407
+ class InternationalText < Base
408
+ attr_accessor :keyword, :text, :language_tag, :translated_keyword, :compressed, :compression
409
+
410
+ def initialize(keyword, text, language_tag = "", translated_keyword = "", compressed = ChunkyPNG::UNCOMPRESSED_CONTENT, compression = ChunkyPNG::COMPRESSION_DEFAULT)
411
+ super("iTXt")
412
+ @keyword = keyword
413
+ @text = text
414
+ @language_tag = language_tag
415
+ @translated_keyword = translated_keyword
416
+ @compressed = compressed
417
+ @compression = compression
418
+ end
419
+
420
+ # Reads the iTXt chunk.
421
+ # @param type [String] The four character chunk type indicator (= "iTXt").
422
+ # @param content [String] The content read from the chunk.
423
+ # @return [ChunkyPNG::Chunk::InternationalText] The new End chunk instance.
424
+ # @raise [ChunkyPNG::InvalidUTF8] If the chunk contains data that is not UTF8-encoded text.
425
+ # @raise [ChunkyPNG::NotSupported] If the chunk refers to an unsupported compression method.
426
+ # Currently uncompressed data and deflate are supported.
427
+ def self.read(type, content)
428
+ keyword, compressed, compression, language_tag, translated_keyword, text = content.unpack("Z*CCZ*Z*a*")
429
+ raise ChunkyPNG::NotSupported, "Compression flag #{compressed.inspect} not supported!" unless compressed == ChunkyPNG::UNCOMPRESSED_CONTENT || compressed == ChunkyPNG::COMPRESSED_CONTENT
430
+ raise ChunkyPNG::NotSupported, "Compression method #{compression.inspect} not supported!" unless compression == ChunkyPNG::COMPRESSION_DEFAULT
431
+
432
+ text = Zlib::Inflate.inflate(text) if compressed == ChunkyPNG::COMPRESSED_CONTENT
433
+
434
+ text.force_encoding("utf-8")
435
+ raise ChunkyPNG::InvalidUTF8, "Invalid unicode encountered in iTXt chunk" unless text.valid_encoding?
436
+
437
+ translated_keyword.force_encoding("utf-8")
438
+ raise ChunkyPNG::InvalidUTF8, "Invalid unicode encountered in iTXt chunk" unless translated_keyword.valid_encoding?
439
+
440
+ new(keyword, text, language_tag, translated_keyword, compressed, compression)
441
+ end
442
+
443
+ # Assembles the content to write to the stream for this chunk.
444
+ # @return [String] The binary content that should be written to the datastream.
445
+ def content
446
+ text_field = text.encode("utf-8")
447
+ text_field = compressed == ChunkyPNG::COMPRESSED_CONTENT ? Zlib::Deflate.deflate(text_field) : text_field
448
+
449
+ [keyword, compressed, compression, language_tag, translated_keyword.encode("utf-8"), text_field].pack("Z*CCZ*Z*a*")
450
+ end
378
451
  end
379
452
 
380
453
  # Maps chunk types to classes, based on the four byte chunk type indicator
@@ -385,15 +458,15 @@ module ChunkyPNG
385
458
  #
386
459
  # @see ChunkyPNG::Chunk.read
387
460
  CHUNK_TYPES = {
388
- 'IHDR' => Header,
389
- 'IEND' => End,
390
- 'IDAT' => ImageData,
391
- 'PLTE' => Palette,
392
- 'tRNS' => Transparency,
393
- 'tEXt' => Text,
394
- 'zTXt' => CompressedText,
395
- 'iTXt' => InternationalText,
396
- 'pHYs' => Physical,
461
+ "IHDR" => Header,
462
+ "IEND" => End,
463
+ "IDAT" => ImageData,
464
+ "PLTE" => Palette,
465
+ "tRNS" => Transparency,
466
+ "tEXt" => Text,
467
+ "zTXt" => CompressedText,
468
+ "iTXt" => InternationalText,
469
+ "pHYs" => Physical,
397
470
  }
398
471
  end
399
472
  end