chunky_png 1.3.9 → 1.3.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.standard.yml +16 -0
- data/.travis.yml +8 -6
- data/.yardopts +1 -1
- data/CHANGELOG.rdoc +9 -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 +19 -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 +22 -24
- 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 +55 -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 +38 -7
- 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
|