chunky_png 1.3.11 → 1.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.standard.yml +16 -0
- data/.travis.yml +5 -5
- data/.yardopts +1 -1
- data/CHANGELOG.rdoc +5 -1
- data/CONTRIBUTING.rdoc +17 -8
- data/Gemfile +3 -3
- data/LICENSE +1 -1
- data/README.md +6 -1
- data/Rakefile +3 -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 +15 -15
- data/lib/chunky_png.rb +16 -25
- data/lib/chunky_png/canvas.rb +28 -27
- data/lib/chunky_png/canvas/adam7_interlacing.rb +14 -10
- data/lib/chunky_png/canvas/data_url_exporting.rb +1 -3
- data/lib/chunky_png/canvas/data_url_importing.rb +1 -3
- data/lib/chunky_png/canvas/drawing.rb +28 -43
- data/lib/chunky_png/canvas/masking.rb +12 -14
- data/lib/chunky_png/canvas/operations.rb +26 -24
- data/lib/chunky_png/canvas/png_decoding.rb +36 -32
- data/lib/chunky_png/canvas/png_encoding.rb +106 -100
- data/lib/chunky_png/canvas/resampling.rb +26 -33
- data/lib/chunky_png/canvas/stream_exporting.rb +6 -8
- data/lib/chunky_png/canvas/stream_importing.rb +6 -8
- data/lib/chunky_png/chunk.rb +69 -60
- data/lib/chunky_png/color.rb +211 -206
- data/lib/chunky_png/datastream.rb +20 -22
- data/lib/chunky_png/dimension.rb +16 -11
- data/lib/chunky_png/image.rb +9 -11
- data/lib/chunky_png/palette.rb +4 -9
- data/lib/chunky_png/point.rb +25 -26
- data/lib/chunky_png/rmagick.rb +8 -10
- data/lib/chunky_png/vector.rb +26 -29
- data/lib/chunky_png/version.rb +1 -1
- 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 +57 -52
- data/spec/chunky_png/color_spec.rb +115 -114
- data/spec/chunky_png/datastream_spec.rb +49 -51
- 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/spec_helper.rb +6 -10
- data/tasks/benchmarks.rake +7 -8
- metadata +34 -5
- data/lib/chunky_png/compatibility.rb +0 -15
@@ -15,8 +15,8 @@ module ChunkyPNG
|
|
15
15
|
# the current instance intact, use {#grayscale} instead.
|
16
16
|
#
|
17
17
|
# @return [ChunkyPNG::Canvas] Returns itself, converted to grayscale.
|
18
|
-
# @see
|
19
|
-
# @see
|
18
|
+
# @see #grayscale
|
19
|
+
# @see ChunkyPNG::Color#to_grayscale
|
20
20
|
def grayscale!
|
21
21
|
pixels.map! { |pixel| ChunkyPNG::Color.to_grayscale(pixel) }
|
22
22
|
self
|
@@ -29,8 +29,8 @@ module ChunkyPNG
|
|
29
29
|
#
|
30
30
|
# @return [ChunkyPNG::Canvas] A copy of the canvas, converted to
|
31
31
|
# grayscale.
|
32
|
-
# @see
|
33
|
-
# @see
|
32
|
+
# @see #grayscale!
|
33
|
+
# @see ChunkyPNG::Color#to_grayscale
|
34
34
|
def grayscale
|
35
35
|
dup.grayscale!
|
36
36
|
end
|
@@ -56,11 +56,14 @@ module ChunkyPNG
|
|
56
56
|
|
57
57
|
for y in 0...other.height do
|
58
58
|
for x in 0...other.width do
|
59
|
-
set_pixel(
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
59
|
+
set_pixel(
|
60
|
+
x + offset_x,
|
61
|
+
y + offset_y,
|
62
|
+
ChunkyPNG::Color.compose(
|
63
|
+
other.get_pixel(x, y),
|
64
|
+
get_pixel(x + offset_x, y + offset_y)
|
65
|
+
)
|
66
|
+
)
|
64
67
|
end
|
65
68
|
end
|
66
69
|
self
|
@@ -169,17 +172,16 @@ module ChunkyPNG
|
|
169
172
|
# coordinates are bigger then the original image.
|
170
173
|
def crop!(x, y, crop_width, crop_height)
|
171
174
|
if crop_width + x > width
|
172
|
-
raise ChunkyPNG::OutOfBounds,
|
175
|
+
raise ChunkyPNG::OutOfBounds, "Original image width is too small!"
|
173
176
|
end
|
174
177
|
if crop_height + y > height
|
175
|
-
raise ChunkyPNG::OutOfBounds,
|
178
|
+
raise ChunkyPNG::OutOfBounds, "Original image height is too small!"
|
176
179
|
end
|
177
180
|
|
178
181
|
if crop_width == width && x == 0
|
179
182
|
# We only need to crop off the top and/or bottom, so we can take a
|
180
183
|
# shortcut.
|
181
|
-
replace_canvas!(crop_width, crop_height,
|
182
|
-
pixels.slice(y * width, width * crop_height))
|
184
|
+
replace_canvas!(crop_width, crop_height, pixels.slice(y * width, width * crop_height))
|
183
185
|
else
|
184
186
|
new_pixels = []
|
185
187
|
for cy in 0...crop_height do
|
@@ -221,8 +223,8 @@ module ChunkyPNG
|
|
221
223
|
self
|
222
224
|
end
|
223
225
|
|
224
|
-
|
225
|
-
|
226
|
+
alias flip! flip_horizontally!
|
227
|
+
alias flip flip_horizontally
|
226
228
|
|
227
229
|
# Flips the image vertically, leaving the original intact.
|
228
230
|
#
|
@@ -253,8 +255,8 @@ module ChunkyPNG
|
|
253
255
|
self
|
254
256
|
end
|
255
257
|
|
256
|
-
|
257
|
-
|
258
|
+
alias mirror! flip_vertically!
|
259
|
+
alias mirror flip_vertically
|
258
260
|
|
259
261
|
# Returns a new canvas instance that is rotated 90 degrees clockwise.
|
260
262
|
#
|
@@ -278,8 +280,8 @@ module ChunkyPNG
|
|
278
280
|
replace_canvas!(height, width, new_pixels)
|
279
281
|
end
|
280
282
|
|
281
|
-
|
282
|
-
|
283
|
+
alias rotate_clockwise rotate_right
|
284
|
+
alias rotate_clockwise! rotate_right!
|
283
285
|
|
284
286
|
# Returns an image that is rotated 90 degrees counter-clockwise.
|
285
287
|
#
|
@@ -305,8 +307,8 @@ module ChunkyPNG
|
|
305
307
|
replace_canvas!(height, width, new_pixels)
|
306
308
|
end
|
307
309
|
|
308
|
-
|
309
|
-
|
310
|
+
alias rotate_counter_clockwise rotate_left
|
311
|
+
alias rotate_counter_clockwise! rotate_left!
|
310
312
|
|
311
313
|
# Rotates the image 180 degrees.
|
312
314
|
#
|
@@ -389,11 +391,11 @@ module ChunkyPNG
|
|
389
391
|
# applied.
|
390
392
|
# @raise [ChunkyPNG::OutOfBounds] when the other image doesn't fit.
|
391
393
|
def check_size_constraints!(other, offset_x, offset_y)
|
392
|
-
if width
|
393
|
-
raise ChunkyPNG::OutOfBounds,
|
394
|
+
if width < other.width + offset_x
|
395
|
+
raise ChunkyPNG::OutOfBounds, "Background image width is too small!"
|
394
396
|
end
|
395
397
|
if height < other.height + offset_y
|
396
|
-
raise ChunkyPNG::OutOfBounds,
|
398
|
+
raise ChunkyPNG::OutOfBounds, "Background image height is too small!"
|
397
399
|
end
|
398
400
|
end
|
399
401
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
module ChunkyPNG
|
2
2
|
class Canvas
|
3
|
-
|
4
3
|
# The PNGDecoding contains methods for decoding PNG datastreams to create a
|
5
4
|
# Canvas object. The datastream can be provided as filename, string or IO
|
6
5
|
# stream.
|
@@ -28,7 +27,6 @@ module ChunkyPNG
|
|
28
27
|
# @see ChunkyPNG::Canvas::PNGEncoding
|
29
28
|
# @see http://www.w3.org/TR/PNG/ The W3C PNG format specification
|
30
29
|
module PNGDecoding
|
31
|
-
|
32
30
|
# Decodes a Canvas from a PNG encoded string.
|
33
31
|
# @param [String] str The string to read from.
|
34
32
|
# @return [ChunkyPNG::Canvas] The canvas decoded from the PNG encoded string.
|
@@ -36,7 +34,7 @@ module ChunkyPNG
|
|
36
34
|
from_datastream(ChunkyPNG::Datastream.from_blob(str))
|
37
35
|
end
|
38
36
|
|
39
|
-
|
37
|
+
alias from_string from_blob
|
40
38
|
|
41
39
|
# Decodes a Canvas from a PNG encoded file.
|
42
40
|
# @param [String] filename The file to read from.
|
@@ -52,7 +50,7 @@ module ChunkyPNG
|
|
52
50
|
from_datastream(ChunkyPNG::Datastream.from_io(io))
|
53
51
|
end
|
54
52
|
|
55
|
-
|
53
|
+
alias from_stream from_io
|
56
54
|
|
57
55
|
# Decodes the Canvas from a PNG datastream instance.
|
58
56
|
# @param [ChunkyPNG::Datastream] ds The datastream to decode.
|
@@ -85,7 +83,7 @@ module ChunkyPNG
|
|
85
83
|
# color mode and interlacing mode.
|
86
84
|
# @param [String] stream The pixelstream to read from.
|
87
85
|
# @param [Integer] width The width of the image.
|
88
|
-
# @param [Integer]
|
86
|
+
# @param [Integer] height The height of the image.
|
89
87
|
# @param [Integer] color_mode The color mode of the encoded pixelstream.
|
90
88
|
# @param [Integer] depth The bit depth of the pixel samples.
|
91
89
|
# @param [Integer] interlace The interlace method of the encoded pixelstream.
|
@@ -96,13 +94,13 @@ module ChunkyPNG
|
|
96
94
|
raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for decoding!" if decoding_palette && !decoding_palette.can_decode?
|
97
95
|
|
98
96
|
image = case interlace
|
99
|
-
when ChunkyPNG::INTERLACING_NONE
|
100
|
-
when ChunkyPNG::INTERLACING_ADAM7
|
97
|
+
when ChunkyPNG::INTERLACING_NONE then decode_png_without_interlacing(stream, width, height, color_mode, depth, decoding_palette)
|
98
|
+
when ChunkyPNG::INTERLACING_ADAM7 then decode_png_with_adam7_interlacing(stream, width, height, color_mode, depth, decoding_palette)
|
101
99
|
else raise ChunkyPNG::NotSupported, "Don't know how the handle interlacing method #{interlace}!"
|
102
100
|
end
|
103
101
|
|
104
102
|
image.pixels.map! { |c| c == transparent_color ? ChunkyPNG::Color::TRANSPARENT : c } if transparent_color
|
105
|
-
|
103
|
+
image
|
106
104
|
end
|
107
105
|
|
108
106
|
protected
|
@@ -147,7 +145,7 @@ module ChunkyPNG
|
|
147
145
|
# the value will be modded by 2 to enforce this.
|
148
146
|
# @return [Integer] The extracted 4bit value (0..15)
|
149
147
|
def decode_png_extract_4bit_value(byte, index)
|
150
|
-
|
148
|
+
index & 0x01 == 0 ? ((byte & 0xf0) >> 4) : (byte & 0x0f)
|
151
149
|
end
|
152
150
|
|
153
151
|
# Extract 2 consecutive bits from a byte.
|
@@ -205,7 +203,6 @@ module ChunkyPNG
|
|
205
203
|
value == 0x01 ? 0xff : 0x00
|
206
204
|
end
|
207
205
|
|
208
|
-
|
209
206
|
# Decodes a scanline of a 1-bit, indexed image into a row of pixels.
|
210
207
|
# @param [String] stream The stream to decode from.
|
211
208
|
# @param [Integer] pos The position in the stream on which the scanline starts (including the filter byte).
|
@@ -259,17 +256,21 @@ module ChunkyPNG
|
|
259
256
|
def decode_png_pixels_from_scanline_truecolor_alpha_16bit(stream, pos, width, _decoding_palette)
|
260
257
|
pixels = []
|
261
258
|
stream.unpack("@#{pos + 1}n#{width * 4}").each_slice(4) do |r, g, b, a|
|
262
|
-
pixels << ChunkyPNG::Color.rgba(
|
263
|
-
|
259
|
+
pixels << ChunkyPNG::Color.rgba(
|
260
|
+
decode_png_resample_16bit_value(r),
|
261
|
+
decode_png_resample_16bit_value(g),
|
262
|
+
decode_png_resample_16bit_value(b),
|
263
|
+
decode_png_resample_16bit_value(a),
|
264
|
+
)
|
264
265
|
end
|
265
|
-
|
266
|
+
pixels
|
266
267
|
end
|
267
268
|
|
268
269
|
# Decodes a scanline of an 8-bit, true color image into a row of pixels.
|
269
270
|
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
|
270
271
|
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
|
271
272
|
def decode_png_pixels_from_scanline_truecolor_8bit(stream, pos, width, _decoding_palette)
|
272
|
-
stream.unpack("@#{pos + 1}" << (
|
273
|
+
stream.unpack("@#{pos + 1}" << ("NX" * width)).map { |c| c | 0x000000ff }
|
273
274
|
end
|
274
275
|
|
275
276
|
# Decodes a scanline of a 16-bit, true color image into a row of pixels.
|
@@ -280,7 +281,7 @@ module ChunkyPNG
|
|
280
281
|
stream.unpack("@#{pos + 1}n#{width * 3}").each_slice(3) do |r, g, b|
|
281
282
|
pixels << ChunkyPNG::Color.rgb(decode_png_resample_16bit_value(r), decode_png_resample_16bit_value(g), decode_png_resample_16bit_value(b))
|
282
283
|
end
|
283
|
-
|
284
|
+
pixels
|
284
285
|
end
|
285
286
|
|
286
287
|
# Decodes a scanline of an 8-bit, grayscale image with transparency into a row of pixels.
|
@@ -298,7 +299,7 @@ module ChunkyPNG
|
|
298
299
|
stream.unpack("@#{pos + 1}n#{width * 2}").each_slice(2) do |g, a|
|
299
300
|
pixels << ChunkyPNG::Color.grayscale_alpha(decode_png_resample_16bit_value(g), decode_png_resample_16bit_value(a))
|
300
301
|
end
|
301
|
-
|
302
|
+
pixels
|
302
303
|
end
|
303
304
|
|
304
305
|
# Decodes a scanline of a 1-bit, grayscale image into a row of pixels.
|
@@ -353,12 +354,11 @@ module ChunkyPNG
|
|
353
354
|
# @raise [ChunkyPNG::NotSupported] when the color_mode and/or bit depth is not supported.
|
354
355
|
def decode_png_pixels_from_scanline_method(color_mode, depth)
|
355
356
|
decoder_method = case color_mode
|
356
|
-
when ChunkyPNG::COLOR_TRUECOLOR
|
357
|
-
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA
|
358
|
-
when ChunkyPNG::COLOR_INDEXED
|
359
|
-
when ChunkyPNG::COLOR_GRAYSCALE
|
360
|
-
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA
|
361
|
-
else nil
|
357
|
+
when ChunkyPNG::COLOR_TRUECOLOR then :"decode_png_pixels_from_scanline_truecolor_#{depth}bit"
|
358
|
+
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"decode_png_pixels_from_scanline_truecolor_alpha_#{depth}bit"
|
359
|
+
when ChunkyPNG::COLOR_INDEXED then :"decode_png_pixels_from_scanline_indexed_#{depth}bit"
|
360
|
+
when ChunkyPNG::COLOR_GRAYSCALE then :"decode_png_pixels_from_scanline_grayscale_#{depth}bit"
|
361
|
+
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"decode_png_pixels_from_scanline_grayscale_alpha_#{depth}bit"
|
362
362
|
end
|
363
363
|
|
364
364
|
raise ChunkyPNG::NotSupported, "No decoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(decoder_method, true)
|
@@ -379,7 +379,6 @@ module ChunkyPNG
|
|
379
379
|
# @param [ChunkyPNG::Palette] decoding_palette The palette to use to decode colors.
|
380
380
|
# @return (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
|
381
381
|
def decode_png_image_pass(stream, width, height, color_mode, depth, start_pos, decoding_palette)
|
382
|
-
|
383
382
|
pixels = []
|
384
383
|
if width > 0 && height > 0
|
385
384
|
|
@@ -419,11 +418,11 @@ module ChunkyPNG
|
|
419
418
|
# @return [void]
|
420
419
|
def decode_png_str_scanline(stream, pos, prev_pos, line_length, pixel_size)
|
421
420
|
case stream.getbyte(pos)
|
422
|
-
when ChunkyPNG::FILTER_NONE
|
423
|
-
when ChunkyPNG::FILTER_SUB
|
424
|
-
when ChunkyPNG::FILTER_UP
|
425
|
-
when ChunkyPNG::FILTER_AVERAGE
|
426
|
-
when ChunkyPNG::FILTER_PAETH
|
421
|
+
when ChunkyPNG::FILTER_NONE then # rubocop:disable Lint/EmptyWhen # no-op
|
422
|
+
when ChunkyPNG::FILTER_SUB then decode_png_str_scanline_sub(stream, pos, prev_pos, line_length, pixel_size)
|
423
|
+
when ChunkyPNG::FILTER_UP then decode_png_str_scanline_up(stream, pos, prev_pos, line_length, pixel_size)
|
424
|
+
when ChunkyPNG::FILTER_AVERAGE then decode_png_str_scanline_average(stream, pos, prev_pos, line_length, pixel_size)
|
425
|
+
when ChunkyPNG::FILTER_PAETH then decode_png_str_scanline_paeth(stream, pos, prev_pos, line_length, pixel_size)
|
427
426
|
else raise ChunkyPNG::NotSupported, "Unknown filter type: #{stream.getbyte(pos)}!"
|
428
427
|
end
|
429
428
|
end
|
@@ -462,7 +461,7 @@ module ChunkyPNG
|
|
462
461
|
# @return [void]
|
463
462
|
def decode_png_str_scanline_average(stream, pos, prev_pos, line_length, pixel_size)
|
464
463
|
for i in 1..line_length do
|
465
|
-
a =
|
464
|
+
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
|
466
465
|
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
|
467
466
|
stream.setbyte(pos + i, (stream.getbyte(pos + i) + ((a + b) >> 1)) & 0xff)
|
468
467
|
end
|
@@ -475,14 +474,19 @@ module ChunkyPNG
|
|
475
474
|
def decode_png_str_scanline_paeth(stream, pos, prev_pos, line_length, pixel_size)
|
476
475
|
for i in 1..line_length do
|
477
476
|
cur_pos = pos + i
|
478
|
-
a =
|
477
|
+
a = i > pixel_size ? stream.getbyte(cur_pos - pixel_size) : 0
|
479
478
|
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
|
480
|
-
c =
|
479
|
+
c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0
|
481
480
|
p = a + b - c
|
482
481
|
pa = (p - a).abs
|
483
482
|
pb = (p - b).abs
|
484
483
|
pc = (p - c).abs
|
485
|
-
pr =
|
484
|
+
pr = if pa <= pb
|
485
|
+
pa <= pc ? a : c
|
486
|
+
else
|
487
|
+
pb <= pc ? b : c
|
488
|
+
end
|
489
|
+
|
486
490
|
stream.setbyte(cur_pos, (stream.getbyte(cur_pos) + pr) & 0xff)
|
487
491
|
end
|
488
492
|
end
|
@@ -1,17 +1,16 @@
|
|
1
1
|
module ChunkyPNG
|
2
2
|
class Canvas
|
3
|
-
|
4
3
|
# Methods for encoding a Canvas instance into a PNG datastream.
|
5
4
|
#
|
6
5
|
# Overview of the encoding process:
|
7
6
|
#
|
8
7
|
# * The image is split up in scanlines (i.e. rows of pixels);
|
9
8
|
# * 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
|
9
|
+
# * All the pixel bytes in the pixelstream are adjusted using a filtering
|
11
10
|
# method if one is specified.
|
12
11
|
# * Compress the resulting string using deflate compression.
|
13
12
|
# * Split compressed data over one or more PNG chunks.
|
14
|
-
# * These chunks should be embedded in a datastream with at least a IHDR and
|
13
|
+
# * These chunks should be embedded in a datastream with at least a IHDR and
|
15
14
|
# IEND chunk and possibly a PLTE chunk.
|
16
15
|
#
|
17
16
|
# For interlaced images, the initial image is first split into 7 subimages.
|
@@ -21,7 +20,6 @@ module ChunkyPNG
|
|
21
20
|
# @see ChunkyPNG::Canvas::PNGDecoding
|
22
21
|
# @see http://www.w3.org/TR/PNG/ The W3C PNG format specification
|
23
22
|
module PNGEncoding
|
24
|
-
|
25
23
|
# The palette used for encoding the image.This is only in used for images
|
26
24
|
# that get encoded using indexed colors.
|
27
25
|
# @return [ChunkyPNG::Palette]
|
@@ -40,28 +38,28 @@ module ChunkyPNG
|
|
40
38
|
# @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream)
|
41
39
|
# @return [void]
|
42
40
|
def save(filename, constraints = {})
|
43
|
-
File.open(filename,
|
41
|
+
File.open(filename, "wb") { |io| write(io, constraints) }
|
44
42
|
end
|
45
|
-
|
43
|
+
|
46
44
|
# Encoded the canvas to a PNG formatted string.
|
47
45
|
# @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream)
|
48
46
|
# @return [String] The PNG encoded canvas as string.
|
49
47
|
def to_blob(constraints = {})
|
50
48
|
to_datastream(constraints).to_blob
|
51
49
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
50
|
+
|
51
|
+
alias to_string to_blob
|
52
|
+
alias to_s to_blob
|
55
53
|
|
56
54
|
# Converts this Canvas to a datastream, so that it can be saved as a PNG image.
|
57
55
|
# @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.
|
56
|
+
# This can either be a hash with different constraints, or a symbol which acts as a
|
57
|
+
# preset for some constraints. If no constraints are given, ChunkyPNG will decide
|
58
|
+
# for itself how to best create the PNG datastream.
|
61
59
|
# Supported presets are <tt>:fast_rgba</tt> for quickly saving images with transparency,
|
62
60
|
# <tt>:fast_rgb</tt> for quickly saving opaque images, and <tt>:best_compression</tt> to
|
63
61
|
# obtain the smallest possible filesize.
|
64
|
-
# @option constraints [Fixnum] :color_mode The color mode to use. Use one of the
|
62
|
+
# @option constraints [Fixnum] :color_mode The color mode to use. Use one of the
|
65
63
|
# ChunkyPNG::COLOR_* constants.
|
66
64
|
# @option constraints [true, false] :interlace Whether to use interlacing.
|
67
65
|
# @option constraints [Fixnum] :compression The compression level for Zlib. This can be a
|
@@ -75,8 +73,13 @@ module ChunkyPNG
|
|
75
73
|
encoding = determine_png_encoding(constraints)
|
76
74
|
|
77
75
|
ds = Datastream.new
|
78
|
-
ds.header_chunk = Chunk::Header.new(
|
79
|
-
|
76
|
+
ds.header_chunk = Chunk::Header.new(
|
77
|
+
width: width,
|
78
|
+
height: height,
|
79
|
+
color: encoding[:color_mode],
|
80
|
+
depth: encoding[:bit_depth],
|
81
|
+
interlace: encoding[:interlace]
|
82
|
+
)
|
80
83
|
|
81
84
|
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
|
82
85
|
ds.palette_chunk = encoding_palette.to_plte_chunk
|
@@ -85,43 +88,42 @@ module ChunkyPNG
|
|
85
88
|
data = encode_png_pixelstream(encoding[:color_mode], encoding[:bit_depth], encoding[:interlace], encoding[:filtering])
|
86
89
|
ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression])
|
87
90
|
ds.end_chunk = Chunk::End.new
|
88
|
-
|
91
|
+
ds
|
89
92
|
end
|
90
93
|
|
91
94
|
protected
|
92
95
|
|
93
|
-
# Determines the best possible PNG encoding variables for this image, by analyzing
|
96
|
+
# Determines the best possible PNG encoding variables for this image, by analyzing
|
94
97
|
# the colors used for the image.
|
95
98
|
#
|
96
|
-
# You can provide constraints for the encoding variables by passing a hash with
|
99
|
+
# You can provide constraints for the encoding variables by passing a hash with
|
97
100
|
# encoding variables to this method.
|
98
101
|
#
|
99
102
|
# @param [Hash, Symbol] constraints The constraints for the encoding. This can be a
|
100
103
|
# Hash or a preset symbol.
|
101
104
|
# @return [Hash] A hash with encoding options for {ChunkyPNG::Canvas::PNGEncoding#to_datastream}
|
102
105
|
def determine_png_encoding(constraints = {})
|
103
|
-
|
104
106
|
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
|
107
|
+
when :fast_rgb then {color_mode: ChunkyPNG::COLOR_TRUECOLOR, compression: Zlib::BEST_SPEED}
|
108
|
+
when :fast_rgba then {color_mode: ChunkyPNG::COLOR_TRUECOLOR_ALPHA, compression: Zlib::BEST_SPEED}
|
109
|
+
when :best_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_PAETH}
|
110
|
+
when :good_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_NONE}
|
111
|
+
when :no_compression then {compression: Zlib::NO_COMPRESSION}
|
112
|
+
when :black_and_white then {color_mode: ChunkyPNG::COLOR_GRAYSCALE, bit_depth: 1}
|
113
|
+
when Hash then constraints
|
112
114
|
else raise ChunkyPNG::Exception, "Unknown encoding preset: #{constraints.inspect}"
|
113
115
|
end
|
114
116
|
|
115
117
|
# Do not create a palette when the encoding is given and does not require a palette.
|
116
118
|
if encoding[:color_mode]
|
117
119
|
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
|
118
|
-
self.encoding_palette =
|
119
|
-
encoding[:bit_depth] ||=
|
120
|
+
self.encoding_palette = palette
|
121
|
+
encoding[:bit_depth] ||= encoding_palette.determine_bit_depth
|
120
122
|
else
|
121
123
|
encoding[:bit_depth] ||= 8
|
122
124
|
end
|
123
125
|
else
|
124
|
-
self.encoding_palette =
|
126
|
+
self.encoding_palette = palette
|
125
127
|
suggested_color_mode, suggested_bit_depth = encoding_palette.best_color_settings
|
126
128
|
encoding[:color_mode] ||= suggested_color_mode
|
127
129
|
encoding[:bit_depth] ||= suggested_bit_depth
|
@@ -131,35 +133,34 @@ module ChunkyPNG
|
|
131
133
|
encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION
|
132
134
|
|
133
135
|
encoding[:interlace] = case encoding[:interlace]
|
134
|
-
when nil, false
|
135
|
-
when true
|
136
|
+
when nil, false then ChunkyPNG::INTERLACING_NONE
|
137
|
+
when true then ChunkyPNG::INTERLACING_ADAM7
|
136
138
|
else encoding[:interlace]
|
137
139
|
end
|
138
140
|
|
139
141
|
encoding[:filtering] ||= case encoding[:compression]
|
140
|
-
when Zlib::BEST_COMPRESSION
|
141
|
-
when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED
|
142
|
+
when Zlib::BEST_COMPRESSION then ChunkyPNG::FILTER_PAETH
|
143
|
+
when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED then ChunkyPNG::FILTER_NONE
|
142
144
|
else ChunkyPNG::FILTER_UP
|
143
145
|
end
|
144
|
-
|
146
|
+
encoding
|
145
147
|
end
|
146
|
-
|
147
|
-
# Encodes the canvas according to the PNG format specification with a given color
|
148
|
+
|
149
|
+
# Encodes the canvas according to the PNG format specification with a given color
|
148
150
|
# mode, possibly with interlacing.
|
149
151
|
# @param [Integer] color_mode The color mode to use for encoding.
|
150
152
|
# @param [Integer] bit_depth The bit depth of the image.
|
151
153
|
# @param [Integer] interlace The interlacing method to use.
|
152
154
|
# @return [String] The PNG encoded canvas as string.
|
153
155
|
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
|
156
|
+
if color_mode == ChunkyPNG::COLOR_INDEXED
|
156
157
|
raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" if encoding_palette.nil? || !encoding_palette.can_encode?
|
157
158
|
raise ChunkyPNG::ExpectationFailed, "This palette has too many colors!" if encoding_palette.size > (1 << bit_depth)
|
158
159
|
end
|
159
160
|
|
160
161
|
case interlace
|
161
|
-
when ChunkyPNG::INTERLACING_NONE
|
162
|
-
when ChunkyPNG::INTERLACING_ADAM7
|
162
|
+
when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(color_mode, bit_depth, filtering)
|
163
|
+
when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(color_mode, bit_depth, filtering)
|
163
164
|
else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!"
|
164
165
|
end
|
165
166
|
end
|
@@ -175,7 +176,7 @@ module ChunkyPNG
|
|
175
176
|
stream
|
176
177
|
end
|
177
178
|
|
178
|
-
# Encodes the canvas according to the PNG format specification with a given color
|
179
|
+
# Encodes the canvas according to the PNG format specification with a given color
|
179
180
|
# mode and Adam7 interlacing.
|
180
181
|
#
|
181
182
|
# This method will split the original canvas in 7 smaller canvases and encode them
|
@@ -201,42 +202,42 @@ module ChunkyPNG
|
|
201
202
|
# @param [Integer] bit_depth The bit depth of the image.
|
202
203
|
# @param [Integer] filtering The filtering method to use.
|
203
204
|
def encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering)
|
204
|
-
|
205
205
|
start_pos = stream.bytesize
|
206
206
|
pixel_size = Color.pixel_bytesize(color_mode)
|
207
207
|
line_width = Color.scanline_bytesize(color_mode, bit_depth, width)
|
208
|
-
|
208
|
+
|
209
209
|
# Determine the filter method
|
210
210
|
encode_method = encode_png_pixels_to_scanline_method(color_mode, bit_depth)
|
211
211
|
filter_method = case filtering
|
212
|
-
when ChunkyPNG::
|
213
|
-
when ChunkyPNG::
|
214
|
-
when ChunkyPNG::
|
215
|
-
when ChunkyPNG::
|
216
|
-
|
212
|
+
when ChunkyPNG::FILTER_NONE then nil
|
213
|
+
when ChunkyPNG::FILTER_SUB then :encode_png_str_scanline_sub
|
214
|
+
when ChunkyPNG::FILTER_UP then :encode_png_str_scanline_up
|
215
|
+
when ChunkyPNG::FILTER_AVERAGE then :encode_png_str_scanline_average
|
216
|
+
when ChunkyPNG::FILTER_PAETH then :encode_png_str_scanline_paeth
|
217
|
+
else raise ArgumentError, "Filtering method #{filtering} is not supported"
|
217
218
|
end
|
218
|
-
|
219
|
+
|
219
220
|
0.upto(height - 1) do |y|
|
220
221
|
stream << send(encode_method, row(y))
|
221
222
|
end
|
222
|
-
|
223
|
+
|
223
224
|
# Now, apply filtering if any
|
224
225
|
if filter_method
|
225
226
|
(height - 1).downto(0) do |y|
|
226
227
|
pos = start_pos + y * (line_width + 1)
|
227
|
-
prev_pos =
|
228
|
+
prev_pos = y == 0 ? nil : pos - (line_width + 1)
|
228
229
|
send(filter_method, stream, pos, prev_pos, line_width, pixel_size)
|
229
230
|
end
|
230
231
|
end
|
231
232
|
end
|
232
|
-
|
233
|
+
|
233
234
|
# Encodes a line of pixels using 8-bit truecolor mode.
|
234
235
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
235
236
|
# @return [String] The encoded scanline as binary string
|
236
237
|
def encode_png_pixels_to_scanline_truecolor_8bit(pixels)
|
237
|
-
pixels.pack(
|
238
|
+
pixels.pack("x" + ("NX" * width))
|
238
239
|
end
|
239
|
-
|
240
|
+
|
240
241
|
# Encodes a line of pixels using 8-bit truecolor alpha mode.
|
241
242
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
242
243
|
# @return [String] The encoded scanline as binary string
|
@@ -250,50 +251,54 @@ module ChunkyPNG
|
|
250
251
|
def encode_png_pixels_to_scanline_indexed_1bit(pixels)
|
251
252
|
chars = []
|
252
253
|
pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8|
|
253
|
-
chars << (
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
254
|
+
chars << (
|
255
|
+
(encoding_palette.index(p1) << 7) |
|
256
|
+
(encoding_palette.index(p2) << 6) |
|
257
|
+
(encoding_palette.index(p3) << 5) |
|
258
|
+
(encoding_palette.index(p4) << 4) |
|
259
|
+
(encoding_palette.index(p5) << 3) |
|
260
|
+
(encoding_palette.index(p6) << 2) |
|
261
|
+
(encoding_palette.index(p7) << 1) |
|
262
|
+
encoding_palette.index(p8)
|
263
|
+
)
|
261
264
|
end
|
262
|
-
chars.pack(
|
265
|
+
chars.pack("xC*")
|
263
266
|
end
|
264
|
-
|
267
|
+
|
265
268
|
# Encodes a line of pixels using 2-bit indexed mode.
|
266
269
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
267
270
|
# @return [String] The encoded scanline as binary string
|
268
271
|
def encode_png_pixels_to_scanline_indexed_2bit(pixels)
|
269
272
|
chars = []
|
270
273
|
pixels.each_slice(4) do |p1, p2, p3, p4|
|
271
|
-
chars << (
|
272
|
-
|
273
|
-
|
274
|
-
|
274
|
+
chars << (
|
275
|
+
(encoding_palette.index(p1) << 6) |
|
276
|
+
(encoding_palette.index(p2) << 4) |
|
277
|
+
(encoding_palette.index(p3) << 2) |
|
278
|
+
encoding_palette.index(p4)
|
279
|
+
)
|
275
280
|
end
|
276
|
-
chars.pack(
|
281
|
+
chars.pack("xC*")
|
277
282
|
end
|
278
|
-
|
283
|
+
|
279
284
|
# Encodes a line of pixels using 4-bit indexed mode.
|
280
285
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
281
286
|
# @return [String] The encoded scanline as binary string
|
282
287
|
def encode_png_pixels_to_scanline_indexed_4bit(pixels)
|
283
288
|
chars = []
|
284
289
|
pixels.each_slice(2) do |p1, p2|
|
285
|
-
chars << ((encoding_palette.index(p1) << 4) |
|
290
|
+
chars << ((encoding_palette.index(p1) << 4) | encoding_palette.index(p2))
|
286
291
|
end
|
287
|
-
chars.pack(
|
292
|
+
chars.pack("xC*")
|
288
293
|
end
|
289
|
-
|
294
|
+
|
290
295
|
# Encodes a line of pixels using 8-bit indexed mode.
|
291
296
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
292
297
|
# @return [String] The encoded scanline as binary string
|
293
298
|
def encode_png_pixels_to_scanline_indexed_8bit(pixels)
|
294
299
|
pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}")
|
295
300
|
end
|
296
|
-
|
301
|
+
|
297
302
|
# Encodes a line of pixels using 1-bit grayscale mode.
|
298
303
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
299
304
|
# @return [String] The encoded scanline as binary string
|
@@ -309,9 +314,9 @@ module ChunkyPNG
|
|
309
314
|
(p7.nil? ? 0 : (p7 & 0x0000ffff) >> 15 << 1) |
|
310
315
|
(p8.nil? ? 0 : (p8 & 0x0000ffff) >> 15))
|
311
316
|
end
|
312
|
-
chars.pack(
|
317
|
+
chars.pack("xC*")
|
313
318
|
end
|
314
|
-
|
319
|
+
|
315
320
|
# Encodes a line of pixels using 2-bit grayscale mode.
|
316
321
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
317
322
|
# @return [String] The encoded scanline as binary string
|
@@ -323,9 +328,9 @@ module ChunkyPNG
|
|
323
328
|
(p3.nil? ? 0 : (p3 & 0x0000ffff) >> 14 << 2) |
|
324
329
|
(p4.nil? ? 0 : (p4 & 0x0000ffff) >> 14))
|
325
330
|
end
|
326
|
-
chars.pack(
|
331
|
+
chars.pack("xC*")
|
327
332
|
end
|
328
|
-
|
333
|
+
|
329
334
|
# Encodes a line of pixels using 2-bit grayscale mode.
|
330
335
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
331
336
|
# @return [String] The encoded scanline as binary string
|
@@ -334,9 +339,9 @@ module ChunkyPNG
|
|
334
339
|
pixels.each_slice(2) do |p1, p2|
|
335
340
|
chars << ((p1.nil? ? 0 : ((p1 & 0x0000ffff) >> 12) << 4) | (p2.nil? ? 0 : ((p2 & 0x0000ffff) >> 12)))
|
336
341
|
end
|
337
|
-
chars.pack(
|
342
|
+
chars.pack("xC*")
|
338
343
|
end
|
339
|
-
|
344
|
+
|
340
345
|
# Encodes a line of pixels using 8-bit grayscale mode.
|
341
346
|
# @param [Array<Integer>] pixels A row of pixels of the original image.
|
342
347
|
# @return [String] The encoded scanline as binary string
|
@@ -350,8 +355,7 @@ module ChunkyPNG
|
|
350
355
|
def encode_png_pixels_to_scanline_grayscale_alpha_8bit(pixels)
|
351
356
|
pixels.pack("xn#{width}")
|
352
357
|
end
|
353
|
-
|
354
|
-
|
358
|
+
|
355
359
|
# Returns the method name to use to decode scanlines into pixels.
|
356
360
|
# @param [Integer] color_mode The color mode of the image.
|
357
361
|
# @param [Integer] depth The bit depth of the image.
|
@@ -359,19 +363,16 @@ module ChunkyPNG
|
|
359
363
|
# @raise [ChunkyPNG::NotSupported] when the color_mode and/or bit depth is not supported.
|
360
364
|
def encode_png_pixels_to_scanline_method(color_mode, depth)
|
361
365
|
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
|
366
|
+
when ChunkyPNG::COLOR_TRUECOLOR then :"encode_png_pixels_to_scanline_truecolor_#{depth}bit"
|
367
|
+
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit"
|
368
|
+
when ChunkyPNG::COLOR_INDEXED then :"encode_png_pixels_to_scanline_indexed_#{depth}bit"
|
369
|
+
when ChunkyPNG::COLOR_GRAYSCALE then :"encode_png_pixels_to_scanline_grayscale_#{depth}bit"
|
370
|
+
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit"
|
368
371
|
end
|
369
|
-
|
372
|
+
|
370
373
|
raise ChunkyPNG::NotSupported, "No encoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(encoder_method, true)
|
371
374
|
encoder_method
|
372
375
|
end
|
373
|
-
|
374
|
-
|
375
376
|
|
376
377
|
# Encodes a scanline of a pixelstream without filtering. This is a no-op.
|
377
378
|
# @param [String] stream The pixelstream to work on. This string will be modified.
|
@@ -391,7 +392,7 @@ module ChunkyPNG
|
|
391
392
|
# @return [void]
|
392
393
|
def encode_png_str_scanline_sub(stream, pos, prev_pos, line_width, pixel_size)
|
393
394
|
line_width.downto(1) do |i|
|
394
|
-
a =
|
395
|
+
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
|
395
396
|
stream.setbyte(pos + i, (stream.getbyte(pos + i) - a) & 0xff)
|
396
397
|
end
|
397
398
|
stream.setbyte(pos, ChunkyPNG::FILTER_SUB)
|
@@ -407,36 +408,41 @@ module ChunkyPNG
|
|
407
408
|
end
|
408
409
|
stream.setbyte(pos, ChunkyPNG::FILTER_UP)
|
409
410
|
end
|
410
|
-
|
411
|
+
|
411
412
|
# Encodes a scanline of a pixelstream using AVERAGE filtering. This will modify the stream.
|
412
413
|
# @param (see #encode_png_str_scanline_none)
|
413
414
|
# @return [void]
|
414
415
|
def encode_png_str_scanline_average(stream, pos, prev_pos, line_width, pixel_size)
|
415
416
|
line_width.downto(1) do |i|
|
416
|
-
a =
|
417
|
+
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
|
417
418
|
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
|
418
419
|
stream.setbyte(pos + i, (stream.getbyte(pos + i) - ((a + b) >> 1)) & 0xff)
|
419
420
|
end
|
420
421
|
stream.setbyte(pos, ChunkyPNG::FILTER_AVERAGE)
|
421
422
|
end
|
422
|
-
|
423
|
+
|
423
424
|
# Encodes a scanline of a pixelstream using PAETH filtering. This will modify the stream.
|
424
425
|
# @param (see #encode_png_str_scanline_none)
|
425
426
|
# @return [void]
|
426
427
|
def encode_png_str_scanline_paeth(stream, pos, prev_pos, line_width, pixel_size)
|
427
428
|
line_width.downto(1) do |i|
|
428
|
-
a =
|
429
|
-
b =
|
430
|
-
c =
|
429
|
+
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
|
430
|
+
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
|
431
|
+
c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0
|
431
432
|
p = a + b - c
|
432
433
|
pa = (p - a).abs
|
433
434
|
pb = (p - b).abs
|
434
435
|
pc = (p - c).abs
|
435
|
-
pr =
|
436
|
+
pr = if pa <= pb && pa <= pc
|
437
|
+
a
|
438
|
+
else
|
439
|
+
pb <= pc ? b : c
|
440
|
+
end
|
441
|
+
|
436
442
|
stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff)
|
437
443
|
end
|
438
444
|
stream.setbyte(pos, ChunkyPNG::FILTER_PAETH)
|
439
|
-
end
|
445
|
+
end
|
440
446
|
end
|
441
447
|
end
|
442
448
|
end
|