chunky_png 1.3.7 → 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 +20 -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 +170 -55
- data/lib/chunky_png/color.rb +218 -212
- data/lib/chunky_png/datastream.rb +29 -29
- 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 +49 -43
- 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 +110 -13
- 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,17 +1,18 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
class Canvas
|
3
|
-
|
4
5
|
# Methods for encoding a Canvas instance into a PNG datastream.
|
5
6
|
#
|
6
7
|
# Overview of the encoding process:
|
7
8
|
#
|
8
9
|
# * The image is split up in scanlines (i.e. rows of pixels);
|
9
10
|
# * All pixels are encoded as a pixelstream, based on the color mode.
|
10
|
-
# * All the pixel bytes in the pixelstream are adjusted using a filtering
|
11
|
+
# * All the pixel bytes in the pixelstream are adjusted using a filtering
|
11
12
|
# method if one is specified.
|
12
13
|
# * Compress the resulting string using deflate compression.
|
13
14
|
# * Split compressed data over one or more PNG chunks.
|
14
|
-
# * These chunks should be embedded in a datastream with at least a IHDR and
|
15
|
+
# * These chunks should be embedded in a datastream with at least a IHDR and
|
15
16
|
# IEND chunk and possibly a PLTE chunk.
|
16
17
|
#
|
17
18
|
# For interlaced images, the initial image is first split into 7 subimages.
|
@@ -19,9 +20,8 @@ module ChunkyPNG
|
|
19
20
|
# before the compression step.
|
20
21
|
#
|
21
22
|
# @see ChunkyPNG::Canvas::PNGDecoding
|
22
|
-
# @see
|
23
|
+
# @see https://www.w3.org/TR/PNG/ The W3C PNG format specification
|
23
24
|
module PNGEncoding
|
24
|
-
|
25
25
|
# The palette used for encoding the image.This is only in used for images
|
26
26
|
# that get encoded using indexed colors.
|
27
27
|
# @return [ChunkyPNG::Palette]
|
@@ -40,28 +40,28 @@ module ChunkyPNG
|
|
40
40
|
# @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream)
|
41
41
|
# @return [void]
|
42
42
|
def save(filename, constraints = {})
|
43
|
-
File.open(filename,
|
43
|
+
File.open(filename, "wb") { |io| write(io, constraints) }
|
44
44
|
end
|
45
|
-
|
45
|
+
|
46
46
|
# Encoded the canvas to a PNG formatted string.
|
47
47
|
# @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream)
|
48
48
|
# @return [String] The PNG encoded canvas as string.
|
49
49
|
def to_blob(constraints = {})
|
50
50
|
to_datastream(constraints).to_blob
|
51
51
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
52
|
+
|
53
|
+
alias to_string to_blob
|
54
|
+
alias to_s to_blob
|
55
55
|
|
56
56
|
# Converts this Canvas to a datastream, so that it can be saved as a PNG image.
|
57
57
|
# @param [Hash, Symbol] constraints The constraints to use when encoding the canvas.
|
58
|
-
# This can either be a hash with different constraints, or a symbol which acts as a
|
59
|
-
# preset for some constraints. If no constraints are given, ChunkyPNG will decide
|
60
|
-
# for itself how to best create the PNG datastream.
|
58
|
+
# This can either be a hash with different constraints, or a symbol which acts as a
|
59
|
+
# preset for some constraints. If no constraints are given, ChunkyPNG will decide
|
60
|
+
# for itself how to best create the PNG datastream.
|
61
61
|
# Supported presets are <tt>:fast_rgba</tt> for quickly saving images with transparency,
|
62
62
|
# <tt>:fast_rgb</tt> for quickly saving opaque images, and <tt>:best_compression</tt> to
|
63
63
|
# obtain the smallest possible filesize.
|
64
|
-
# @option constraints [Fixnum] :color_mode The color mode to use. Use one of the
|
64
|
+
# @option constraints [Fixnum] :color_mode The color mode to use. Use one of the
|
65
65
|
# ChunkyPNG::COLOR_* constants.
|
66
66
|
# @option constraints [true, false] :interlace Whether to use interlacing.
|
67
67
|
# @option constraints [Fixnum] :compression The compression level for Zlib. This can be a
|
@@ -75,8 +75,13 @@ module ChunkyPNG
|
|
75
75
|
encoding = determine_png_encoding(constraints)
|
76
76
|
|
77
77
|
ds = Datastream.new
|
78
|
-
ds.header_chunk = Chunk::Header.new(
|
79
|
-
|
78
|
+
ds.header_chunk = Chunk::Header.new(
|
79
|
+
width: width,
|
80
|
+
height: height,
|
81
|
+
color: encoding[:color_mode],
|
82
|
+
depth: encoding[:bit_depth],
|
83
|
+
interlace: encoding[:interlace]
|
84
|
+
)
|
80
85
|
|
81
86
|
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
|
82
87
|
ds.palette_chunk = encoding_palette.to_plte_chunk
|
@@ -85,43 +90,42 @@ module ChunkyPNG
|
|
85
90
|
data = encode_png_pixelstream(encoding[:color_mode], encoding[:bit_depth], encoding[:interlace], encoding[:filtering])
|
86
91
|
ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression])
|
87
92
|
ds.end_chunk = Chunk::End.new
|
88
|
-
|
93
|
+
ds
|
89
94
|
end
|
90
95
|
|
91
96
|
protected
|
92
97
|
|
93
|
-
# Determines the best possible PNG encoding variables for this image, by analyzing
|
98
|
+
# Determines the best possible PNG encoding variables for this image, by analyzing
|
94
99
|
# the colors used for the image.
|
95
100
|
#
|
96
|
-
# You can provide constraints for the encoding variables by passing a hash with
|
101
|
+
# You can provide constraints for the encoding variables by passing a hash with
|
97
102
|
# encoding variables to this method.
|
98
103
|
#
|
99
104
|
# @param [Hash, Symbol] constraints The constraints for the encoding. This can be a
|
100
105
|
# Hash or a preset symbol.
|
101
106
|
# @return [Hash] A hash with encoding options for {ChunkyPNG::Canvas::PNGEncoding#to_datastream}
|
102
107
|
def determine_png_encoding(constraints = {})
|
103
|
-
|
104
108
|
encoding = case constraints
|
105
|
-
when :fast_rgb
|
106
|
-
when :fast_rgba
|
107
|
-
when :best_compression
|
108
|
-
when :good_compression
|
109
|
-
when :no_compression
|
110
|
-
when :black_and_white
|
111
|
-
when Hash
|
109
|
+
when :fast_rgb then {color_mode: ChunkyPNG::COLOR_TRUECOLOR, compression: Zlib::BEST_SPEED}
|
110
|
+
when :fast_rgba then {color_mode: ChunkyPNG::COLOR_TRUECOLOR_ALPHA, compression: Zlib::BEST_SPEED}
|
111
|
+
when :best_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_PAETH}
|
112
|
+
when :good_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_NONE}
|
113
|
+
when :no_compression then {compression: Zlib::NO_COMPRESSION}
|
114
|
+
when :black_and_white then {color_mode: ChunkyPNG::COLOR_GRAYSCALE, bit_depth: 1}
|
115
|
+
when Hash then constraints
|
112
116
|
else raise ChunkyPNG::Exception, "Unknown encoding preset: #{constraints.inspect}"
|
113
117
|
end
|
114
118
|
|
115
119
|
# Do not create a palette when the encoding is given and does not require a palette.
|
116
120
|
if encoding[:color_mode]
|
117
121
|
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
|
118
|
-
self.encoding_palette =
|
119
|
-
encoding[:bit_depth] ||=
|
122
|
+
self.encoding_palette = palette
|
123
|
+
encoding[:bit_depth] ||= encoding_palette.determine_bit_depth
|
120
124
|
else
|
121
125
|
encoding[:bit_depth] ||= 8
|
122
126
|
end
|
123
127
|
else
|
124
|
-
self.encoding_palette =
|
128
|
+
self.encoding_palette = palette
|
125
129
|
suggested_color_mode, suggested_bit_depth = encoding_palette.best_color_settings
|
126
130
|
encoding[:color_mode] ||= suggested_color_mode
|
127
131
|
encoding[:bit_depth] ||= suggested_bit_depth
|
@@ -131,35 +135,34 @@ module ChunkyPNG
|
|
131
135
|
encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION
|
132
136
|
|
133
137
|
encoding[:interlace] = case encoding[:interlace]
|
134
|
-
when nil, false
|
135
|
-
when true
|
138
|
+
when nil, false then ChunkyPNG::INTERLACING_NONE
|
139
|
+
when true then ChunkyPNG::INTERLACING_ADAM7
|
136
140
|
else encoding[:interlace]
|
137
141
|
end
|
138
142
|
|
139
143
|
encoding[:filtering] ||= case encoding[:compression]
|
140
|
-
when Zlib::BEST_COMPRESSION
|
141
|
-
when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED
|
144
|
+
when Zlib::BEST_COMPRESSION then ChunkyPNG::FILTER_PAETH
|
145
|
+
when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED then ChunkyPNG::FILTER_NONE
|
142
146
|
else ChunkyPNG::FILTER_UP
|
143
147
|
end
|
144
|
-
|
148
|
+
encoding
|
145
149
|
end
|
146
|
-
|
147
|
-
# Encodes the canvas according to the PNG format specification with a given color
|
150
|
+
|
151
|
+
# Encodes the canvas according to the PNG format specification with a given color
|
148
152
|
# mode, possibly with interlacing.
|
149
153
|
# @param [Integer] color_mode The color mode to use for encoding.
|
150
154
|
# @param [Integer] bit_depth The bit depth of the image.
|
151
155
|
# @param [Integer] interlace The interlacing method to use.
|
152
156
|
# @return [String] The PNG encoded canvas as string.
|
153
157
|
def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, bit_depth = 8, interlace = ChunkyPNG::INTERLACING_NONE, filtering = ChunkyPNG::FILTER_NONE)
|
154
|
-
|
155
|
-
if color_mode == ChunkyPNG::COLOR_INDEXED
|
158
|
+
if color_mode == ChunkyPNG::COLOR_INDEXED
|
156
159
|
raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" if encoding_palette.nil? || !encoding_palette.can_encode?
|
157
160
|
raise ChunkyPNG::ExpectationFailed, "This palette has too many colors!" if encoding_palette.size > (1 << bit_depth)
|
158
161
|
end
|
159
162
|
|
160
163
|
case interlace
|
161
|
-
when ChunkyPNG::INTERLACING_NONE
|
162
|
-
when ChunkyPNG::INTERLACING_ADAM7
|
164
|
+
when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(color_mode, bit_depth, filtering)
|
165
|
+
when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(color_mode, bit_depth, filtering)
|
163
166
|
else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!"
|
164
167
|
end
|
165
168
|
end
|
@@ -170,12 +173,12 @@ module ChunkyPNG
|
|
170
173
|
# @param [Integer] filtering The filtering method to use.
|
171
174
|
# @return [String] The PNG encoded canvas as string.
|
172
175
|
def encode_png_image_without_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE)
|
173
|
-
stream =
|
176
|
+
stream = "".b
|
174
177
|
encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering)
|
175
178
|
stream
|
176
179
|
end
|
177
180
|
|
178
|
-
# Encodes the canvas according to the PNG format specification with a given color
|
181
|
+
# Encodes the canvas according to the PNG format specification with a given color
|
179
182
|
# mode and Adam7 interlacing.
|
180
183
|
#
|
181
184
|
# This method will split the original canvas in 7 smaller canvases and encode them
|
@@ -186,7 +189,7 @@ module ChunkyPNG
|
|
186
189
|
# @param [Integer] filtering The filtering method to use.
|
187
190
|
# @return [String] The PNG encoded canvas as string.
|
188
191
|
def encode_png_image_with_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE)
|
189
|
-
stream =
|
192
|
+
stream = "".b
|
190
193
|
0.upto(6) do |pass|
|
191
194
|
subcanvas = self.class.adam7_extract_pass(pass, self)
|
192
195
|
subcanvas.encoding_palette = encoding_palette
|
@@ -201,42 +204,42 @@ module ChunkyPNG
|
|
201
204
|
# @param [Integer] bit_depth The bit depth of the image.
|
202
205
|
# @param [Integer] filtering The filtering method to use.
|
203
206
|
def encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering)
|
204
|
-
|
205
207
|
start_pos = stream.bytesize
|
206
208
|
pixel_size = Color.pixel_bytesize(color_mode)
|
207
209
|
line_width = Color.scanline_bytesize(color_mode, bit_depth, width)
|
208
|
-
|
210
|
+
|
209
211
|
# Determine the filter method
|
210
212
|
encode_method = encode_png_pixels_to_scanline_method(color_mode, bit_depth)
|
211
213
|
filter_method = case filtering
|
212
|
-
when ChunkyPNG::
|
213
|
-
when ChunkyPNG::
|
214
|
-
when ChunkyPNG::
|
215
|
-
when ChunkyPNG::
|
216
|
-
|
214
|
+
when ChunkyPNG::FILTER_NONE then nil
|
215
|
+
when ChunkyPNG::FILTER_SUB then :encode_png_str_scanline_sub
|
216
|
+
when ChunkyPNG::FILTER_UP then :encode_png_str_scanline_up
|
217
|
+
when ChunkyPNG::FILTER_AVERAGE then :encode_png_str_scanline_average
|
218
|
+
when ChunkyPNG::FILTER_PAETH then :encode_png_str_scanline_paeth
|
219
|
+
else raise ArgumentError, "Filtering method #{filtering} is not supported"
|
217
220
|
end
|
218
|
-
|
221
|
+
|
219
222
|
0.upto(height - 1) do |y|
|
220
223
|
stream << send(encode_method, row(y))
|
221
224
|
end
|
222
|
-
|
225
|
+
|
223
226
|
# Now, apply filtering if any
|
224
227
|
if filter_method
|
225
228
|
(height - 1).downto(0) do |y|
|
226
229
|
pos = start_pos + y * (line_width + 1)
|
227
|
-
prev_pos =
|
230
|
+
prev_pos = y == 0 ? nil : pos - (line_width + 1)
|
228
231
|
send(filter_method, stream, pos, prev_pos, line_width, pixel_size)
|
229
232
|
end
|
230
233
|
end
|
231
234
|
end
|
232
|
-
|
235
|
+
|
233
236
|
# Encodes a line of pixels using 8-bit truecolor mode.
|
234
237
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
235
238
|
# @return [String] The encoded scanline as binary string
|
236
239
|
def encode_png_pixels_to_scanline_truecolor_8bit(pixels)
|
237
|
-
pixels.pack(
|
240
|
+
pixels.pack("x" + ("NX" * width))
|
238
241
|
end
|
239
|
-
|
242
|
+
|
240
243
|
# Encodes a line of pixels using 8-bit truecolor alpha mode.
|
241
244
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
242
245
|
# @return [String] The encoded scanline as binary string
|
@@ -250,50 +253,54 @@ module ChunkyPNG
|
|
250
253
|
def encode_png_pixels_to_scanline_indexed_1bit(pixels)
|
251
254
|
chars = []
|
252
255
|
pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8|
|
253
|
-
chars << (
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
256
|
+
chars << (
|
257
|
+
(encoding_palette.index(p1) << 7) |
|
258
|
+
(encoding_palette.index(p2) << 6) |
|
259
|
+
(encoding_palette.index(p3) << 5) |
|
260
|
+
(encoding_palette.index(p4) << 4) |
|
261
|
+
(encoding_palette.index(p5) << 3) |
|
262
|
+
(encoding_palette.index(p6) << 2) |
|
263
|
+
(encoding_palette.index(p7) << 1) |
|
264
|
+
encoding_palette.index(p8)
|
265
|
+
)
|
261
266
|
end
|
262
|
-
chars.pack(
|
267
|
+
chars.pack("xC*")
|
263
268
|
end
|
264
|
-
|
269
|
+
|
265
270
|
# Encodes a line of pixels using 2-bit indexed mode.
|
266
271
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
267
272
|
# @return [String] The encoded scanline as binary string
|
268
273
|
def encode_png_pixels_to_scanline_indexed_2bit(pixels)
|
269
274
|
chars = []
|
270
275
|
pixels.each_slice(4) do |p1, p2, p3, p4|
|
271
|
-
chars << (
|
272
|
-
|
273
|
-
|
274
|
-
|
276
|
+
chars << (
|
277
|
+
(encoding_palette.index(p1) << 6) |
|
278
|
+
(encoding_palette.index(p2) << 4) |
|
279
|
+
(encoding_palette.index(p3) << 2) |
|
280
|
+
encoding_palette.index(p4)
|
281
|
+
)
|
275
282
|
end
|
276
|
-
chars.pack(
|
283
|
+
chars.pack("xC*")
|
277
284
|
end
|
278
|
-
|
285
|
+
|
279
286
|
# Encodes a line of pixels using 4-bit indexed mode.
|
280
287
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
281
288
|
# @return [String] The encoded scanline as binary string
|
282
289
|
def encode_png_pixels_to_scanline_indexed_4bit(pixels)
|
283
290
|
chars = []
|
284
291
|
pixels.each_slice(2) do |p1, p2|
|
285
|
-
chars << ((encoding_palette.index(p1) << 4) |
|
292
|
+
chars << ((encoding_palette.index(p1) << 4) | encoding_palette.index(p2))
|
286
293
|
end
|
287
|
-
chars.pack(
|
294
|
+
chars.pack("xC*")
|
288
295
|
end
|
289
|
-
|
296
|
+
|
290
297
|
# Encodes a line of pixels using 8-bit indexed mode.
|
291
298
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
292
299
|
# @return [String] The encoded scanline as binary string
|
293
300
|
def encode_png_pixels_to_scanline_indexed_8bit(pixels)
|
294
301
|
pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}")
|
295
302
|
end
|
296
|
-
|
303
|
+
|
297
304
|
# Encodes a line of pixels using 1-bit grayscale mode.
|
298
305
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
299
306
|
# @return [String] The encoded scanline as binary string
|
@@ -309,9 +316,9 @@ module ChunkyPNG
|
|
309
316
|
(p7.nil? ? 0 : (p7 & 0x0000ffff) >> 15 << 1) |
|
310
317
|
(p8.nil? ? 0 : (p8 & 0x0000ffff) >> 15))
|
311
318
|
end
|
312
|
-
chars.pack(
|
319
|
+
chars.pack("xC*")
|
313
320
|
end
|
314
|
-
|
321
|
+
|
315
322
|
# Encodes a line of pixels using 2-bit grayscale mode.
|
316
323
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
317
324
|
# @return [String] The encoded scanline as binary string
|
@@ -323,9 +330,9 @@ module ChunkyPNG
|
|
323
330
|
(p3.nil? ? 0 : (p3 & 0x0000ffff) >> 14 << 2) |
|
324
331
|
(p4.nil? ? 0 : (p4 & 0x0000ffff) >> 14))
|
325
332
|
end
|
326
|
-
chars.pack(
|
333
|
+
chars.pack("xC*")
|
327
334
|
end
|
328
|
-
|
335
|
+
|
329
336
|
# Encodes a line of pixels using 2-bit grayscale mode.
|
330
337
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
331
338
|
# @return [String] The encoded scanline as binary string
|
@@ -334,9 +341,9 @@ module ChunkyPNG
|
|
334
341
|
pixels.each_slice(2) do |p1, p2|
|
335
342
|
chars << ((p1.nil? ? 0 : ((p1 & 0x0000ffff) >> 12) << 4) | (p2.nil? ? 0 : ((p2 & 0x0000ffff) >> 12)))
|
336
343
|
end
|
337
|
-
chars.pack(
|
344
|
+
chars.pack("xC*")
|
338
345
|
end
|
339
|
-
|
346
|
+
|
340
347
|
# Encodes a line of pixels using 8-bit grayscale mode.
|
341
348
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
342
349
|
# @return [String] The encoded scanline as binary string
|
@@ -350,8 +357,7 @@ module ChunkyPNG
|
|
350
357
|
def encode_png_pixels_to_scanline_grayscale_alpha_8bit(pixels)
|
351
358
|
pixels.pack("xn#{width}")
|
352
359
|
end
|
353
|
-
|
354
|
-
|
360
|
+
|
355
361
|
# Returns the method name to use to decode scanlines into pixels.
|
356
362
|
# @param [Integer] color_mode The color mode of the image.
|
357
363
|
# @param [Integer] depth The bit depth of the image.
|
@@ -359,19 +365,16 @@ module ChunkyPNG
|
|
359
365
|
# @raise [ChunkyPNG::NotSupported] when the color_mode and/or bit depth is not supported.
|
360
366
|
def encode_png_pixels_to_scanline_method(color_mode, depth)
|
361
367
|
encoder_method = case color_mode
|
362
|
-
when ChunkyPNG::COLOR_TRUECOLOR
|
363
|
-
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA
|
364
|
-
when ChunkyPNG::COLOR_INDEXED
|
365
|
-
when ChunkyPNG::COLOR_GRAYSCALE
|
366
|
-
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA
|
367
|
-
else nil
|
368
|
+
when ChunkyPNG::COLOR_TRUECOLOR then :"encode_png_pixels_to_scanline_truecolor_#{depth}bit"
|
369
|
+
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit"
|
370
|
+
when ChunkyPNG::COLOR_INDEXED then :"encode_png_pixels_to_scanline_indexed_#{depth}bit"
|
371
|
+
when ChunkyPNG::COLOR_GRAYSCALE then :"encode_png_pixels_to_scanline_grayscale_#{depth}bit"
|
372
|
+
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit"
|
368
373
|
end
|
369
|
-
|
374
|
+
|
370
375
|
raise ChunkyPNG::NotSupported, "No encoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(encoder_method, true)
|
371
376
|
encoder_method
|
372
377
|
end
|
373
|
-
|
374
|
-
|
375
378
|
|
376
379
|
# Encodes a scanline of a pixelstream without filtering. This is a no-op.
|
377
380
|
# @param [String] stream The pixelstream to work on. This string will be modified.
|
@@ -391,7 +394,7 @@ module ChunkyPNG
|
|
391
394
|
# @return [void]
|
392
395
|
def encode_png_str_scanline_sub(stream, pos, prev_pos, line_width, pixel_size)
|
393
396
|
line_width.downto(1) do |i|
|
394
|
-
a =
|
397
|
+
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
|
395
398
|
stream.setbyte(pos + i, (stream.getbyte(pos + i) - a) & 0xff)
|
396
399
|
end
|
397
400
|
stream.setbyte(pos, ChunkyPNG::FILTER_SUB)
|
@@ -407,36 +410,41 @@ module ChunkyPNG
|
|
407
410
|
end
|
408
411
|
stream.setbyte(pos, ChunkyPNG::FILTER_UP)
|
409
412
|
end
|
410
|
-
|
413
|
+
|
411
414
|
# Encodes a scanline of a pixelstream using AVERAGE filtering. This will modify the stream.
|
412
415
|
# @param (see #encode_png_str_scanline_none)
|
413
416
|
# @return [void]
|
414
417
|
def encode_png_str_scanline_average(stream, pos, prev_pos, line_width, pixel_size)
|
415
418
|
line_width.downto(1) do |i|
|
416
|
-
a =
|
419
|
+
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
|
417
420
|
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
|
418
421
|
stream.setbyte(pos + i, (stream.getbyte(pos + i) - ((a + b) >> 1)) & 0xff)
|
419
422
|
end
|
420
423
|
stream.setbyte(pos, ChunkyPNG::FILTER_AVERAGE)
|
421
424
|
end
|
422
|
-
|
425
|
+
|
423
426
|
# Encodes a scanline of a pixelstream using PAETH filtering. This will modify the stream.
|
424
427
|
# @param (see #encode_png_str_scanline_none)
|
425
428
|
# @return [void]
|
426
429
|
def encode_png_str_scanline_paeth(stream, pos, prev_pos, line_width, pixel_size)
|
427
430
|
line_width.downto(1) do |i|
|
428
|
-
a =
|
429
|
-
b =
|
430
|
-
c =
|
431
|
+
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
|
432
|
+
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
|
433
|
+
c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0
|
431
434
|
p = a + b - c
|
432
435
|
pa = (p - a).abs
|
433
436
|
pb = (p - b).abs
|
434
437
|
pc = (p - c).abs
|
435
|
-
pr =
|
438
|
+
pr = if pa <= pb && pa <= pc
|
439
|
+
a
|
440
|
+
else
|
441
|
+
pb <= pc ? b : c
|
442
|
+
end
|
443
|
+
|
436
444
|
stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff)
|
437
445
|
end
|
438
446
|
stream.setbyte(pos, ChunkyPNG::FILTER_PAETH)
|
439
|
-
end
|
447
|
+
end
|
440
448
|
end
|
441
449
|
end
|
442
450
|
end
|
@@ -1,10 +1,8 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen-string-literal: true
|
3
2
|
|
4
3
|
module ChunkyPNG
|
5
4
|
class Canvas
|
6
|
-
|
7
|
-
# The ChunkyPNG::Canvas::Resampling module defines methods to perform image resampling to
|
5
|
+
# The ChunkyPNG::Canvas::Resampling module defines methods to perform image resampling to
|
8
6
|
# a {ChunkyPNG::Canvas}.
|
9
7
|
#
|
10
8
|
# Currently, only the nearest neighbor algorithm is implemented. Bilinear and cubic
|
@@ -12,22 +10,21 @@ module ChunkyPNG
|
|
12
10
|
#
|
13
11
|
# @see ChunkyPNG::Canvas
|
14
12
|
module Resampling
|
15
|
-
|
16
13
|
# Integer Interpolation between two values
|
17
14
|
#
|
18
15
|
# Used for generating indicies for interpolation (eg, nearest
|
19
16
|
# neighbour).
|
20
17
|
#
|
21
|
-
# @param [Integer] width The width of the source
|
18
|
+
# @param [Integer] width The width of the source
|
22
19
|
# @param [Integer] new_width The width of the destination
|
23
20
|
# @return [Array<Integer>] An Array of Integer indicies
|
24
21
|
def steps(width, new_width)
|
25
22
|
indicies, residues = steps_residues(width, new_width)
|
26
|
-
|
23
|
+
|
27
24
|
for i in 1..new_width
|
28
|
-
indicies[i-1] = (indicies[i-1] + (residues[i-1] + 127)/255)
|
25
|
+
indicies[i - 1] = (indicies[i - 1] + (residues[i - 1] + 127) / 255)
|
29
26
|
end
|
30
|
-
|
27
|
+
indicies
|
31
28
|
end
|
32
29
|
|
33
30
|
# Fractional Interpolation between two values
|
@@ -39,9 +36,9 @@ module ChunkyPNG
|
|
39
36
|
# @param [Integer] new_width The width of the destination
|
40
37
|
# @return [Array<Integer>, Array<Integer>] Two arrays of indicies and residues
|
41
38
|
def steps_residues(width, new_width)
|
42
|
-
indicies = Array.new(new_width,
|
43
|
-
residues = Array.new(new_width,
|
44
|
-
|
39
|
+
indicies = Array.new(new_width, nil)
|
40
|
+
residues = Array.new(new_width, nil)
|
41
|
+
|
45
42
|
# This works by accumulating the fractional error and
|
46
43
|
# overflowing when necessary.
|
47
44
|
|
@@ -49,15 +46,15 @@ module ChunkyPNG
|
|
49
46
|
# 2 * new_width
|
50
47
|
base_step = width / new_width
|
51
48
|
err_step = (width % new_width) << 1
|
52
|
-
denominator =
|
53
|
-
|
49
|
+
denominator = new_width << 1
|
50
|
+
|
54
51
|
# Initial pixel
|
55
52
|
index = (width - new_width) / denominator
|
56
53
|
err = (width - new_width) % denominator
|
57
54
|
|
58
55
|
for i in 1..new_width
|
59
|
-
indicies[i-1] = index
|
60
|
-
residues[i-1] = (255.0 * err.to_f / denominator.to_f).round
|
56
|
+
indicies[i - 1] = index
|
57
|
+
residues[i - 1] = (255.0 * err.to_f / denominator.to_f).round
|
61
58
|
|
62
59
|
index += base_step
|
63
60
|
err += err_step
|
@@ -67,10 +64,9 @@ module ChunkyPNG
|
|
67
64
|
end
|
68
65
|
end
|
69
66
|
|
70
|
-
|
67
|
+
[indicies, residues]
|
71
68
|
end
|
72
69
|
|
73
|
-
|
74
70
|
# Resamples the canvas using nearest neighbor interpolation.
|
75
71
|
# @param [Integer] new_width The width of the resampled canvas.
|
76
72
|
# @param [Integer] new_height The height of the resampled canvas.
|
@@ -79,8 +75,7 @@ module ChunkyPNG
|
|
79
75
|
steps_x = steps(width, new_width)
|
80
76
|
steps_y = steps(height, new_height)
|
81
77
|
|
82
|
-
|
83
|
-
pixels = Array.new(new_width*new_height)
|
78
|
+
pixels = Array.new(new_width * new_height)
|
84
79
|
i = 0
|
85
80
|
for y in steps_y
|
86
81
|
for x in steps_x
|
@@ -88,10 +83,10 @@ module ChunkyPNG
|
|
88
83
|
i += 1
|
89
84
|
end
|
90
85
|
end
|
91
|
-
|
86
|
+
|
92
87
|
replace_canvas!(new_width.to_i, new_height.to_i, pixels)
|
93
88
|
end
|
94
|
-
|
89
|
+
|
95
90
|
def resample_nearest_neighbor(new_width, new_height)
|
96
91
|
dup.resample_nearest_neighbor!(new_width, new_height)
|
97
92
|
end
|
@@ -104,19 +99,19 @@ module ChunkyPNG
|
|
104
99
|
index_x, interp_x = steps_residues(width, new_width)
|
105
100
|
index_y, interp_y = steps_residues(height, new_height)
|
106
101
|
|
107
|
-
pixels = Array.new(new_width*new_height)
|
102
|
+
pixels = Array.new(new_width * new_height)
|
108
103
|
i = 0
|
109
104
|
for y in 1..new_height
|
110
105
|
# Clamp the indicies to the edges of the image
|
111
|
-
y1 = [index_y[y-1], 0].max
|
112
|
-
y2 = [index_y[y-1] + 1, height - 1].min
|
113
|
-
y_residue = interp_y[y-1]
|
106
|
+
y1 = [index_y[y - 1], 0].max
|
107
|
+
y2 = [index_y[y - 1] + 1, height - 1].min
|
108
|
+
y_residue = interp_y[y - 1]
|
114
109
|
|
115
110
|
for x in 1..new_width
|
116
111
|
# Clamp the indicies to the edges of the image
|
117
|
-
x1 = [index_x[x-1], 0].max
|
118
|
-
x2 = [index_x[x-1] + 1, width - 1].min
|
119
|
-
x_residue = interp_x[x-1]
|
112
|
+
x1 = [index_x[x - 1], 0].max
|
113
|
+
x2 = [index_x[x - 1] + 1, width - 1].min
|
114
|
+
x_residue = interp_x[x - 1]
|
120
115
|
|
121
116
|
pixel_11 = get_pixel(x1, y1)
|
122
117
|
pixel_21 = get_pixel(x2, y1)
|
@@ -139,9 +134,9 @@ module ChunkyPNG
|
|
139
134
|
def resample_bilinear(new_width, new_height)
|
140
135
|
dup.resample_bilinear!(new_width, new_height)
|
141
136
|
end
|
142
|
-
|
143
|
-
|
144
|
-
|
137
|
+
|
138
|
+
alias resample resample_nearest_neighbor
|
139
|
+
alias resize resample
|
145
140
|
end
|
146
141
|
end
|
147
142
|
end
|