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.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +35 -0
- data/.standard.yml +16 -0
- data/.yardopts +1 -1
- data/CHANGELOG.rdoc +16 -4
- data/CONTRIBUTING.rdoc +17 -8
- data/Gemfile +12 -4
- data/LICENSE +1 -1
- data/README.md +15 -9
- data/Rakefile +5 -3
- data/benchmarks/decoding_benchmark.rb +17 -17
- data/benchmarks/encoding_benchmark.rb +22 -19
- data/benchmarks/filesize_benchmark.rb +6 -6
- data/bin/rake +29 -0
- data/bin/standardrb +29 -0
- data/chunky_png.gemspec +21 -13
- data/docs/.gitignore +3 -0
- data/docs/CNAME +1 -0
- data/docs/_config.yml +9 -0
- data/docs/_posts/2010-01-14-memory-efficiency-when-using-ruby.md +136 -0
- data/docs/_posts/2010-01-17-ode-to-array-pack-and-string-unpack.md +82 -0
- data/docs/_posts/2014-11-07-the-value-of-a-pure-ruby-library.md +61 -0
- data/docs/index.md +88 -0
- data/lib/chunky_png/canvas/adam7_interlacing.rb +16 -10
- data/lib/chunky_png/canvas/data_url_exporting.rb +3 -3
- data/lib/chunky_png/canvas/data_url_importing.rb +3 -3
- data/lib/chunky_png/canvas/drawing.rb +30 -43
- data/lib/chunky_png/canvas/masking.rb +14 -14
- data/lib/chunky_png/canvas/operations.rb +28 -24
- data/lib/chunky_png/canvas/png_decoding.rb +39 -33
- data/lib/chunky_png/canvas/png_encoding.rb +111 -103
- data/lib/chunky_png/canvas/resampling.rb +27 -32
- data/lib/chunky_png/canvas/stream_exporting.rb +8 -8
- data/lib/chunky_png/canvas/stream_importing.rb +8 -8
- data/lib/chunky_png/canvas.rb +31 -28
- data/lib/chunky_png/chunk.rb +142 -69
- data/lib/chunky_png/color.rb +218 -212
- data/lib/chunky_png/datastream.rb +24 -30
- data/lib/chunky_png/dimension.rb +18 -11
- data/lib/chunky_png/image.rb +11 -11
- data/lib/chunky_png/palette.rb +13 -14
- data/lib/chunky_png/point.rb +27 -26
- data/lib/chunky_png/rmagick.rb +10 -10
- data/lib/chunky_png/vector.rb +28 -29
- data/lib/chunky_png/version.rb +3 -1
- data/lib/chunky_png.rb +46 -45
- data/spec/chunky_png/canvas/adam7_interlacing_spec.rb +20 -21
- data/spec/chunky_png/canvas/data_url_exporting_spec.rb +8 -5
- data/spec/chunky_png/canvas/data_url_importing_spec.rb +5 -6
- data/spec/chunky_png/canvas/drawing_spec.rb +46 -38
- data/spec/chunky_png/canvas/masking_spec.rb +15 -16
- data/spec/chunky_png/canvas/operations_spec.rb +68 -67
- data/spec/chunky_png/canvas/png_decoding_spec.rb +37 -38
- data/spec/chunky_png/canvas/png_encoding_spec.rb +59 -50
- data/spec/chunky_png/canvas/resampling_spec.rb +19 -21
- data/spec/chunky_png/canvas/stream_exporting_spec.rb +47 -27
- data/spec/chunky_png/canvas/stream_importing_spec.rb +10 -11
- data/spec/chunky_png/canvas_spec.rb +63 -52
- data/spec/chunky_png/color_spec.rb +115 -114
- data/spec/chunky_png/datastream_spec.rb +98 -19
- data/spec/chunky_png/dimension_spec.rb +10 -10
- data/spec/chunky_png/image_spec.rb +11 -14
- data/spec/chunky_png/point_spec.rb +21 -23
- data/spec/chunky_png/rmagick_spec.rb +7 -8
- data/spec/chunky_png/vector_spec.rb +21 -17
- data/spec/chunky_png_spec.rb +2 -2
- data/spec/png_suite_spec.rb +35 -40
- data/spec/resources/itxt_chunk.png +0 -0
- data/spec/spec_helper.rb +15 -9
- data/tasks/benchmarks.rake +7 -8
- metadata +65 -25
- data/.travis.yml +0 -16
- 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(
|
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(
|
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(
|
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(
|
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(
|
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 =
|
21
|
+
unpacker = "NX" * (width * height)
|
22
22
|
pixels = string.unpack(unpacker).map { |color| color | 0x000000ff }
|
23
|
-
|
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
|
-
|
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"
|
57
|
-
|
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
|
-
|
73
|
+
new(width, height, string.unpack("V*"))
|
74
74
|
end
|
75
75
|
end
|
76
76
|
end
|
data/lib/chunky_png/canvas.rb
CHANGED
@@ -1,14 +1,16 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
8
|
-
require
|
9
|
-
require
|
10
|
-
require
|
11
|
-
require
|
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
|
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.
|
82
|
-
|
83
|
-
|
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]
|
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]
|
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
|
-
|
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.
|
278
|
-
|
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
|
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
|
364
|
+
if width != matrix_width
|
362
365
|
raise ChunkyPNG::ExpectationFailed,
|
363
|
-
|
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
|
-
|
370
|
+
"The height of the matrix does not match the canvas height!"
|
368
371
|
end
|
369
372
|
true
|
370
373
|
end
|
data/lib/chunky_png/chunk.rb
CHANGED
@@ -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(
|
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(
|
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(
|
78
|
-
io << [Zlib.crc32(content, Zlib.crc32(type))].pack(
|
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, :
|
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(
|
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(
|
141
|
-
new(
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
[
|
155
|
-
|
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(
|
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 [
|
186
|
+
# @raise [ChunkyPNG::ExpectationFailed] Raises an exception if the content was not empty.
|
174
187
|
def self.read(type, content)
|
175
|
-
raise ExpectationFailed,
|
176
|
-
|
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
|
-
|
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(
|
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
|
-
|
228
|
-
|
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(
|
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
|
-
[
|
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.
|
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(
|
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(
|
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(
|
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(
|
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(
|
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
|
-
[
|
318
|
-
|
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
|
-
#
|
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,
|
332
|
-
super(
|
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,
|
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,
|
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(
|
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
|
-
#
|
353
|
-
#
|
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(
|
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
|
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
|
-
# @
|
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 <
|
377
|
-
|
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
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
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
|