chunky_png 1.3.12 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +35 -0
- data/CHANGELOG.rdoc +3 -3
- data/Gemfile +10 -2
- data/README.md +13 -12
- data/Rakefile +2 -0
- data/chunky_png.gemspec +5 -1
- 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 +2 -0
- data/lib/chunky_png/canvas/data_url_exporting.rb +2 -0
- data/lib/chunky_png/canvas/data_url_importing.rb +2 -0
- data/lib/chunky_png/canvas/drawing.rb +2 -0
- data/lib/chunky_png/canvas/masking.rb +2 -0
- data/lib/chunky_png/canvas/operations.rb +2 -0
- data/lib/chunky_png/canvas/png_decoding.rb +4 -2
- data/lib/chunky_png/canvas/png_encoding.rb +5 -3
- data/lib/chunky_png/canvas/resampling.rb +2 -0
- data/lib/chunky_png/canvas/stream_exporting.rb +2 -0
- data/lib/chunky_png/canvas/stream_importing.rb +3 -1
- data/lib/chunky_png/canvas.rb +3 -1
- data/lib/chunky_png/chunk.rb +32 -6
- data/lib/chunky_png/color.rb +7 -6
- data/lib/chunky_png/datastream.rb +4 -8
- data/lib/chunky_png/dimension.rb +2 -0
- data/lib/chunky_png/image.rb +2 -0
- data/lib/chunky_png/palette.rb +9 -5
- data/lib/chunky_png/point.rb +2 -0
- data/lib/chunky_png/rmagick.rb +2 -0
- data/lib/chunky_png/vector.rb +2 -0
- data/lib/chunky_png/version.rb +3 -1
- data/lib/chunky_png.rb +3 -7
- data/spec/chunky_png/canvas_spec.rb +6 -0
- metadata +15 -8
- data/.travis.yml +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83fc1d331bcc0afc882b5ff2df1c0601e42f6551cfaff2b7c7ae9d0b4386b122
|
4
|
+
data.tar.gz: 1f80013e9aeb9daa25ed0a968b5d82877483113ebd22442226e0e8ee269a3105
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1fcb07937d255014cba405bc018c9889b5fd7cd652e25f0768b21ef58a48f604033551f3fa822329387a3dcecb46263906c1043ecb5a7e0007fba22f90dbeb5f
|
7
|
+
data.tar.gz: dd57be8a9ce7db0333dad21f03f9ea056feaed4a785f3e0d21b7a1efb3123bacf6fb58f73cbde9d46f2aa2032b39dd5e06e25a4e48aeb9d5dad993ad99f886e9
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# This workflow uses actions that are not certified by GitHub.
|
2
|
+
# They are provided by a third-party and are governed by
|
3
|
+
# separate terms of service, privacy policy, and support
|
4
|
+
# documentation.
|
5
|
+
# This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
|
6
|
+
# For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
|
7
|
+
|
8
|
+
name: Ruby
|
9
|
+
|
10
|
+
on:
|
11
|
+
push:
|
12
|
+
branches: ["master"]
|
13
|
+
pull_request:
|
14
|
+
branches: ["master"]
|
15
|
+
|
16
|
+
jobs:
|
17
|
+
test:
|
18
|
+
runs-on: ubuntu-latest
|
19
|
+
strategy:
|
20
|
+
matrix:
|
21
|
+
ruby: [ '2.5', '2.6', '2.7', 'ruby-head' ]
|
22
|
+
|
23
|
+
steps:
|
24
|
+
- uses: actions/checkout@v2
|
25
|
+
- name: Set up Ruby
|
26
|
+
uses: ruby/setup-ruby@v1
|
27
|
+
with:
|
28
|
+
ruby-version: ${{ matrix.ruby }}
|
29
|
+
- name: Install dependencies
|
30
|
+
run: bundle install
|
31
|
+
- name: Run tests
|
32
|
+
run: bin/rake
|
33
|
+
# Skip the linter for now.
|
34
|
+
# - name: Lint Ruby code
|
35
|
+
# run: bin/standardrb
|
data/CHANGELOG.rdoc
CHANGED
@@ -46,7 +46,7 @@ The file documents the changes to this library over the different versions.
|
|
46
46
|
=== 1.3.4 - 2015-02-16
|
47
47
|
|
48
48
|
- Assert compatibility with Ruby 2.2
|
49
|
-
- Improved documentation using RDoc, so it is included on
|
49
|
+
- Improved documentation using RDoc, so it is included on https://www.rubydoc.info/gems/chunky_png
|
50
50
|
- Update chunkypng.com website; migrate some stuff from the wiki.
|
51
51
|
|
52
52
|
=== 1.3.3 - 2014-10-24
|
@@ -154,7 +154,7 @@ There are some API changes for this release. If you are using <tt>Canvas#compose
|
|
154
154
|
- Added a list of HTML named colors. Get them by calling <tt>ChunkyPNG::Color(:teal)</tt> or <tt>ChunkyPNG::Color('red @ 0.8')</tt>
|
155
155
|
- Added encoding support for 1-, 2-, and 4-bit grayscale images.
|
156
156
|
- Cleaned up auto-detection of color mode settings. It will now choose 1 bit grayscale mode if an image only contains black and white. (The other low bitrate grayscale modes are never chosen automatically.)
|
157
|
-
- RDoc improvements. See
|
157
|
+
- RDoc improvements. See https://rdoc.info/gems/chunky_png.
|
158
158
|
- ChunkyPNG is now also tested on Ruby 1.8.6.
|
159
159
|
|
160
160
|
=== 0.12.0 - 2010-12-12
|
@@ -214,7 +214,7 @@ There are some API changes for this release. If you are using <tt>Canvas#compose
|
|
214
214
|
|
215
215
|
=== 0.7.3 - 2010-04-28
|
216
216
|
|
217
|
-
- Based on the suggestion of [Dirkjan Bussink](
|
217
|
+
- Based on the suggestion of [Dirkjan Bussink](https://github.com/dbussink), introduced custom exception classes:
|
218
218
|
- <tt>ChunkyPNG::SignatureMismatch</tt> is raised when the PNG signature could not be found. Usually this means the the file is not a PNG image.
|
219
219
|
- <tt>ChunkyPNG::CRCMismatch</tt> is raised when the a CRC check for a chunk in the PNG file fails.
|
220
220
|
- <tt>ChunkyPNG::NotSupported</tt> is raised when the PNG image uses a feature that ChunkyPNG does not support.
|
data/Gemfile
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
source "https://rubygems.org"
|
2
4
|
gemspec
|
3
5
|
|
@@ -5,6 +7,12 @@ platforms :jruby do
|
|
5
7
|
gem "jruby-openssl"
|
6
8
|
end
|
7
9
|
|
8
|
-
|
9
|
-
gem "
|
10
|
+
group :jekyll do
|
11
|
+
gem "jekyll", "~> 3.3"
|
12
|
+
gem "kramdown-parser-gfm"
|
10
13
|
end
|
14
|
+
|
15
|
+
group :jekyll_plugins do
|
16
|
+
gem "jekyll-commonmark"
|
17
|
+
gem "jekyll-theme-cayman"
|
18
|
+
end
|
data/README.md
CHANGED
@@ -1,13 +1,13 @@
|
|
1
|
-
# ChunkyPNG
|
1
|
+
# ChunkyPNG
|
2
2
|
|
3
3
|
This library can read and write PNG files. It is written in pure Ruby for
|
4
4
|
maximum portability. Let me rephrase: it does NOT require RMagick or any other
|
5
5
|
memory leaking image library.
|
6
6
|
|
7
|
-
- [Source code](
|
8
|
-
- [RDoc](
|
9
|
-
- [Wiki](
|
10
|
-
- [Issue tracker](
|
7
|
+
- [Source code](https://github.com/wvanbergen/chunky_png/tree/master)
|
8
|
+
- [RDoc](https://rdoc.info/gems/chunky_png)
|
9
|
+
- [Wiki](https://github.com/wvanbergen/chunky_png/wiki)
|
10
|
+
- [Issue tracker](https://github.com/wvanbergen/chunky_png/issues)
|
11
11
|
|
12
12
|
## Features
|
13
13
|
|
@@ -23,9 +23,10 @@ memory leaking image library.
|
|
23
23
|
depending on the hardware)
|
24
24
|
- Reasonably fast for Ruby standards, by only using integer math and a highly
|
25
25
|
optimized saving routine.
|
26
|
+
- Works on every currently supported Ruby version (2.5+)
|
26
27
|
- Interoperability with RMagick if you really have to.
|
27
28
|
|
28
|
-
Also, have a look at [OilyPNG](
|
29
|
+
Also, have a look at [OilyPNG](https://github.com/wvanbergen/oily_png) which
|
29
30
|
is a mixin module that implements some of the ChunkyPNG algorithms in C, which
|
30
31
|
provides a massive speed boost to encoding and decoding.
|
31
32
|
|
@@ -59,11 +60,11 @@ png_stream.each_chunk { |chunk| p chunk.type }
|
|
59
60
|
|
60
61
|
Also check out the screencast on the ChunkyPNG homepage by John Davison,
|
61
62
|
which illustrates basic usage of the library on the [ChunkyPNG
|
62
|
-
website](
|
63
|
+
website](https://chunkypng.com/).
|
63
64
|
|
64
65
|
For more information, see the [project
|
65
66
|
wiki](https://github.com/wvanbergen/chunky_png/wiki) or the [RDOC
|
66
|
-
documentation](
|
67
|
+
documentation](https://www.rubydoc.info/gems/chunky_png).
|
67
68
|
|
68
69
|
## Security warning
|
69
70
|
|
@@ -80,11 +81,11 @@ background processing library.
|
|
80
81
|
|
81
82
|
The library is written by Willem van Bergen for Floorplanner.com, and released
|
82
83
|
under the MIT license (see LICENSE). Please contact me for questions or
|
83
|
-
remarks.
|
84
|
+
remarks.
|
84
85
|
|
85
|
-
I generally consider this library to be feature complete. I will gladly accept
|
86
|
-
patches to fix bugs and improve performance, but I will generally be hesitant
|
87
|
-
to accept new features or API endpoints. Before contributing, please read
|
86
|
+
I generally consider this library to be feature complete. I will gladly accept
|
87
|
+
patches to fix bugs and improve performance, but I will generally be hesitant
|
88
|
+
to accept new features or API endpoints. Before contributing, please read
|
88
89
|
[CONTRIBUTING.rdoc](CONTRIBUTING.rdoc) that explains this in more detail.
|
89
90
|
|
90
91
|
Please check out CHANGELOG.rdoc to see what changed in all versions.
|
data/Rakefile
CHANGED
data/chunky_png.gemspec
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
lib = File.expand_path("../lib", __FILE__)
|
2
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
5
|
require "chunky_png/version"
|
@@ -24,7 +26,7 @@ Gem::Specification.new do |s|
|
|
24
26
|
alpha composition and cropping. Finally, it can import from and export to RMagick for
|
25
27
|
interoperability.
|
26
28
|
|
27
|
-
Also, have a look at OilyPNG at
|
29
|
+
Also, have a look at OilyPNG at https://github.com/wvanbergen/oily_png. OilyPNG is a
|
28
30
|
drop in mixin module that implements some of the ChunkyPNG algorithms in C, which
|
29
31
|
provides a massive speed boost to encoding and decoding.
|
30
32
|
EOT
|
@@ -48,4 +50,6 @@ Gem::Specification.new do |s|
|
|
48
50
|
|
49
51
|
s.files = `git ls-files`.split($/)
|
50
52
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
53
|
+
|
54
|
+
s.required_ruby_version = ">= 2.0"
|
51
55
|
end
|
data/docs/.gitignore
ADDED
data/docs/CNAME
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
chunkypng.com
|
data/docs/_config.yml
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
---
|
2
|
+
author: Willem van Bergen
|
3
|
+
title: Memory efficiency when using Ruby
|
4
|
+
---
|
5
|
+
|
6
|
+
I have been spending some time creating [a pure Ruby PNG library](https://github.com/wvanbergen/chunky_png). For this library, I need to have some representation of the image, which is composed of RGB pixels, supporting an alpha channel. Because images can be composed of a lot of pixels, I want the implementation to be as memory efficient as possible. I also would like decent performance.
|
7
|
+
|
8
|
+
A very naive Ruby implementation for an image represents the red, green, blue and alpha channel using a floating point number between 0.0 and 1.0, and might look something like this:
|
9
|
+
|
10
|
+
{% highlight ruby %}
|
11
|
+
class Pixel
|
12
|
+
attr_reader :r, :g, :b, :a
|
13
|
+
|
14
|
+
def initialize(r, g, b, a = 1.0)
|
15
|
+
@r, @g, @b, @a = r, g, b, a
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Image
|
20
|
+
attr_reader :width, :height
|
21
|
+
|
22
|
+
def initialize(width, height)
|
23
|
+
@width, @height = width, height
|
24
|
+
@pixels = Array.new(width * height)
|
25
|
+
end
|
26
|
+
|
27
|
+
def [](x,y)
|
28
|
+
@pixels[y * width + x]
|
29
|
+
end
|
30
|
+
|
31
|
+
def []=(x,y, pixel)
|
32
|
+
@pixels[y * width + x] = pixel
|
33
|
+
end
|
34
|
+
end
|
35
|
+
{% endhighlight %}
|
36
|
+
|
37
|
+
For a 10×10 image, this representation requires 4 times 100 floating point numbers, which require 8 bytes each. That’s already over 3kB for such a small image just for the floating point numbers! Ouch.
|
38
|
+
|
39
|
+
A simple improvement is to decide that 8-bit color depth is enough in the case, in which case each channel can be represented by an integer between 0 and 255. Storing such a number only costs one byte of memory. Ruby’s Fixnum class typically uses 4-byte integers. If only the 4 channels of one byte each could be combined into a single Fixnum instance… Behold!
|
40
|
+
|
41
|
+
{% highlight ruby %}
|
42
|
+
class Pixel
|
43
|
+
attr_reader :value
|
44
|
+
alias :to_i :value
|
45
|
+
|
46
|
+
def initialize(value)
|
47
|
+
@value = value
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.rgba(r, g, b, a = 255)
|
51
|
+
self.new(r << 24 | g << 16 | b << 8 | a)
|
52
|
+
end
|
53
|
+
|
54
|
+
def r; (@value & 0xff000000) >> 24; end
|
55
|
+
def g; (@value & 0x00ff0000) >> 16; end
|
56
|
+
def b; (@value & 0x0000ff00) >> 8; end
|
57
|
+
def a; (@value & 0x000000ff); end
|
58
|
+
end
|
59
|
+
{% endhighlight %}
|
60
|
+
|
61
|
+
Notice the bit operations, which are extremely fast. This only requires 100 times 4 bytes = 400 bytes for storing the RGBA values for a 10×10 image, an 8 times improvement!
|
62
|
+
|
63
|
+
This implementation wraps every pixel inside an object. This is nice, because I want to access the separate channels of every pixel easily using the r, g, b, and a methods, and every other method that is defined for every pixel. However, a Ruby object instance has an overhead of at least 20 bytes. That’s 20 times 100 is about 2kB for our 10×10 image!
|
64
|
+
|
65
|
+
To get rid of the object overhead, it is possible to simply store the Fixnum value for every pixel, and only wrapping it inside a Pixel object when it is accessed. This can be done by modifying the Image class:
|
66
|
+
|
67
|
+
{% highlight ruby %}
|
68
|
+
class Image
|
69
|
+
# ...
|
70
|
+
|
71
|
+
def [](x,y)
|
72
|
+
Pixel.new(@pixels[y * width + x]) # wrap
|
73
|
+
end
|
74
|
+
|
75
|
+
def []=(x,y, pixel)
|
76
|
+
@pixels[y * width + x] = pixel.to_i # unwrap
|
77
|
+
end
|
78
|
+
end
|
79
|
+
{% endhighlight %}
|
80
|
+
|
81
|
+
As you can see, some simply changes in the representation can really make a difference in the memory usage. Can this representation be improved further?
|
82
|
+
|
83
|
+
## Integer math calculations
|
84
|
+
|
85
|
+
Because we are now using integers to represent a pixel, this can cause problems when the math requires you to use floating point numbers. For example, the formula for [alpha composition](https://en.wikipedia.org/wiki/Alpha_compositing) of two pixels is as follows:
|
86
|
+
|
87
|
+
\\[ C_o = C_a \alpha_a + C_b \alpha_b (1 - \alpha_a) \\]
|
88
|
+
|
89
|
+
in which \\(C_a\\) is the color component of the foreground pixel, \\(\alpha_a\\) the alpha channel of the foreground pixel, \\(C_b\\) and \\(\alpha_b\\) the same values for the background pixel, all of which should be values between 0 and 1.
|
90
|
+
|
91
|
+
A naive implementation could convert the integer numbers to their floating point equivalents:
|
92
|
+
|
93
|
+
{% highlight ruby %}
|
94
|
+
def compose(fg, bg)
|
95
|
+
return bg if fg.a == 0
|
96
|
+
return fg if fg.a == 255
|
97
|
+
|
98
|
+
fg_alpha = fg.a / 255.0
|
99
|
+
bg_alpha = fg.a / 255.0
|
100
|
+
alpha_complement = (1.0 - fg_alpha) * bg_alpha
|
101
|
+
|
102
|
+
new_r = (fg_alpha * fg.r + alpha_complement * bg.r).round
|
103
|
+
new_g = (fg_alpha * fg.g + alpha_complement * bg.g).round
|
104
|
+
new_b = (fg_alpha * fg.b + alpha_complement * bg.b).round
|
105
|
+
new_a = ((fg_alpha + alpha_complement) * 255).round
|
106
|
+
|
107
|
+
Pixel.rgba(new_r, new_g, new_b, new_a)
|
108
|
+
end
|
109
|
+
{% endhighlight %}
|
110
|
+
|
111
|
+
This implementation is already a little bit optimized: no unnecessary conversions and calculations are being performed. However, this composition can be done a lot quicker after realizing that 255 is almost a power of two, in which computers excel because it can use bitwise operators and shifting for some calculations.
|
112
|
+
|
113
|
+
My new approach uses a quicker implementation of multiplication of 8-bit integers that represent floating numbers between 0 and 1:
|
114
|
+
|
115
|
+
{% highlight ruby %}
|
116
|
+
def compose(fg, bg)
|
117
|
+
return bg if fg.a == 0
|
118
|
+
return fg if fg.a == 255
|
119
|
+
|
120
|
+
alpha_complement = multiply(255 - fg.a, bg.a)
|
121
|
+
new_r = multiply(fg.a, fg.r) + multiply(alpha_complement, bg.r)
|
122
|
+
new_g = multiply(fg.a, fg.g) + multiply(alpha_complement, bg.g)
|
123
|
+
new_b = multiply(fg.a, fg.b) + multiply(alpha_complement, bg.b)
|
124
|
+
new_a = fg.a + alpha_complement
|
125
|
+
|
126
|
+
Pixel.rgba(new_r, new_g, new_b, new_a)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Quicker alternative for (a * b / 255.0).round
|
130
|
+
def multiply(a, b)
|
131
|
+
t = a * b + 0x80
|
132
|
+
((t >> 8) + t) >> 8
|
133
|
+
end
|
134
|
+
{% endhighlight %}
|
135
|
+
|
136
|
+
Note that the new implementation is less precise in theory, but this precision is lost anyway because we have to convert the values back to 8 bit RGBA values. Your thoughts?
|
@@ -0,0 +1,82 @@
|
|
1
|
+
---
|
2
|
+
author: Willem van Bergen
|
3
|
+
title: Ode to Array#pack and String#unpack
|
4
|
+
---
|
5
|
+
|
6
|
+
Remember [my last post]({% post_url 2010-01-14-memory-efficiency-when-using-ruby %}), where I representing a pixel with a Fixnum, storing the R, G, B and A value in its 4 bytes of memory? Well, I have been working some more on [my PNG library](https://github.com/wvanbergen/chunky_png) and I am now trying loading and saving an image.
|
7
|
+
|
8
|
+
Using the [PNG specification](https://www.w3.org/TR/PNG/), building a PNG encoder/decoder isn’t that hard, but the required algorithmic calculations make sure that performance in Ruby is less than stellar. I have rewritten all calculations to only use fast integer math (plus, minus, multiply and bitwise operators), but simply the amount of code that is getting executed is slowing Ruby down. What more can I do to improve the performance?
|
9
|
+
|
10
|
+
## Encoding RGBA images
|
11
|
+
|
12
|
+
Optimizing loading images is very hard, because PNG images can have many variations, and taking shortcuts means that some images are no longer supported. Not so with saving images: as long an image is saved using one of the valid variations, every PNG decoder will be able to read the file. Let’s see if it is possible to optimize one of these encoding variations.
|
13
|
+
|
14
|
+
During encoding, the image get splits up into scanlines (rows) of pixels, which in turn get converted into bytes. These bytes can be filtered for optimal compression. For a 3×3 8-bit RGBA image, the result looks like this:
|
15
|
+
|
16
|
+
F Rf Gf Bf Af Rf Gf Bf Af Rf Gf Bf Af
|
17
|
+
F Rf Gf Bf Af Rf Gf Bf Af Rf Gf Bf Af
|
18
|
+
F Rf Gf Bf Af Rf Gf Bf Af Rf Gf Bf Af
|
19
|
+
|
20
|
+
Every line starts with a byte F indicating the filter method, followed by the filtered R, G and B value for every pixel on that line. Now, if we choose filter method 0, which means no filtering, the result looks like this:
|
21
|
+
|
22
|
+
0 Ro Go Bo Ao Ro Go Bo Ao Ro Go Bo Ao
|
23
|
+
0 Ro Go Bo Ao Ro Go Bo Ao Ro Go Bo Ao
|
24
|
+
0 Ro Go Bo Ao Ro Go Bo Ao Ro Go Bo Ao
|
25
|
+
|
26
|
+
Now, the original R, G, B and A byte from the original pixel’s Fixnum, occur in [big-endian or network byte order](https://en.wikipedia.org/wiki/Endianness), starting with the top left pixel, moving left to right and then top to bottom. Exactly like the pixels are stored in our image’s pixel array! This means that we can use the Array#pack method to encode into this format. The Array#pack-notation for this is "xN3" in which x get translated into a null byte, and every N as 4-byte integer in network byte order. For optimal performance, it is best to not split the original array in lines, but to pack the complete pixel array at once. So, we can encode all pixels with this command:
|
27
|
+
|
28
|
+
{% highlight ruby %}
|
29
|
+
pixeldata = pixels.pack("xN#{width}" * height)
|
30
|
+
{% endhighlight %}
|
31
|
+
|
32
|
+
This way, the splitting the image into lines, splitting the pixels into bytes, and filtering the bytes can be skipped. In Ruby 1.8.7, this means a speedup of over 1500% (no typo)! Of course, because no filtering applied, the subsequent compression is not optimal, but that is a tradeoff that I am willing to make.
|
33
|
+
|
34
|
+
## Encoding RGB images
|
35
|
+
|
36
|
+
What about RGB images without alpha channel? We can simply choose to encode these using the RGBA method, but that increases the file size with roughly 25%. Can we fix this somehow?
|
37
|
+
|
38
|
+
The unfiltered pixel data should look something like this:
|
39
|
+
|
40
|
+
0 Ro Go Bo Ro Go Bo Ro Go Bo
|
41
|
+
0 Ro Go Bo Ro Go Bo Ro Go Bo
|
42
|
+
0 Ro Go Bo Ro Go Bo Ro Go Bo
|
43
|
+
|
44
|
+
This means that for every pixel that is encoded as a 4-byte integer, the last byte should be ditched. Luckily, the `Array#pack` method offers a modifier that does just that: `X`. Packing a 3 pixel line can be done with `"xNXNXNX"`. Again we would like to pack the whole pixel array at once:
|
45
|
+
|
46
|
+
{% highlight ruby %}
|
47
|
+
pixeldata = pixels.pack(("x" + ('NX' * width)) * height)
|
48
|
+
{% endhighlight %}
|
49
|
+
|
50
|
+
Because all the encoding steps can get skipped once again, the speed improvement is again 1500%! And the result is 25% smaller than the RGBA method. This method is actually so speedy, that saving an image using Ruby 1.9.1 is only a little bit slower (< 10%) than saving a PNG image using RMagick! See my [performance comparison](https://github.com/wvanbergen/chunky_png/wiki/performance-comparison).
|
51
|
+
|
52
|
+
## Loading image
|
53
|
+
|
54
|
+
Given the promising results of the Array#pack method, using its counterpart String#unpack looks promising for speedy image loading, if you know the image’s size and the encoding format beforehand.
|
55
|
+
|
56
|
+
An RGBA formatted stream can be loaded quickly with this command:
|
57
|
+
|
58
|
+
{% highlight ruby %}
|
59
|
+
pixels = rgba_pixeldata.unpack("N#{width * height}")
|
60
|
+
image = Image.new(width, height, pixels)
|
61
|
+
{% endhighlight %}
|
62
|
+
|
63
|
+
For an RGB formatted stream, we can use the X modifier again, but we have to make sure to set the alpha value for every pixel to 255:
|
64
|
+
|
65
|
+
{% highlight ruby %}
|
66
|
+
pixels = rgb_pixeldata.unpack("NX" * (width * height))
|
67
|
+
pixels.map! { |pixel| pixel | 0x000000ff }
|
68
|
+
image = Image.new(width, height, pixels)
|
69
|
+
{% endhighlight %}
|
70
|
+
|
71
|
+
You can even use little-endian integers to load streams in ABGR format!
|
72
|
+
|
73
|
+
{% highlight ruby %}
|
74
|
+
pixels = abgr_pixeldata.unpack("V#{width * height}")
|
75
|
+
image = Image.new(width, height, pixels)
|
76
|
+
{% endhighlight %}
|
77
|
+
|
78
|
+
Loading pixel data for an image like this is again over 1500% faster than decoding the same PNG image. However, this can only be applied if you have control over the input format of the image.
|
79
|
+
|
80
|
+
## To conclude
|
81
|
+
|
82
|
+
`Array#pack` and `String#unpack` really have increased the performance for my code. If you can apply them for project, don’t hesitate and spread the love! For all other cases, use as little code as possible, and upgrade to Ruby 1.9 for improved algorithmic performance.
|
@@ -0,0 +1,61 @@
|
|
1
|
+
---
|
2
|
+
author: Willem van Bergen
|
3
|
+
title: The value of a pure Ruby library
|
4
|
+
---
|
5
|
+
|
6
|
+
In late 2009, my employer at the time — [Floorplanner](https://www.floorplanner.com) — was struggling with memory leaks in [RMagick](https://www.imagemagick.org/RMagick/doc/), a Ruby wrapper around the image manipulation library [ImageMagick](https://www.imagemagick.org/). Because we only needed a small subset of RMagick's functionality, I decided to write a simple library so we could get rid of RMagick. Not much later, [ChunkyPNG was born](https://github.com/wvanbergen/chunky_png/commit/aa8a9378eedfc02aa1d0d1e05c313badc76594a7).
|
7
|
+
|
8
|
+
Even though ChunkyPNG has grown in scope and complexity to cover the entire PNG standard, it still is a "pure Ruby" library: all of the code is Ruby, and it doesn't have any dependencies besides Ruby itself. Initially, this was purely for practical reasons: I knew Ruby wasn't the fastest language in the world, but I had no idea how to write Ruby C extensions. Performance was not an important concern for the problem at hand, and maybe RMagick being a C extension was the cause of its memory leaks? By writing pure Ruby, I could get results faster and let the Ruby interpreter do the hard work of managing memory for me. <sup>[1]</sup>
|
9
|
+
|
10
|
+
### Performance becomes important
|
11
|
+
|
12
|
+
Mostly as a learning project, I ended up implementing the entire PNG standard. This made the library suitable for a broader set of problems, and more people started using it. Performance then became more important. I put a decent effort into optimizing the memory efficiency by [optimizing storing pixels in memory]({% post_url 2010-01-14-memory-efficiency-when-using-ruby %}), and I boosted performance by [short-circuiting the PNG encoding routine using Array#pack]({% post_url 2010-01-17-ode-to-array-pack-and-string-unpack %}).
|
13
|
+
|
14
|
+
Even though these efforts resulted in sizable improvements, it became clear that there are limits on how far you can push performance in Ruby. The fact that I am implementing a library that by nature requires a lot of memory and computation is not going to change.
|
15
|
+
|
16
|
+
So what are the options? I could recommend RMagick to people asking for more performance, but that is not going to happen after all my ImageMagick bashing. <sup>[2]</sup> In the end, I had to roll up my sleeves and program some C.
|
17
|
+
|
18
|
+
### Being pure Ruby is a feature
|
19
|
+
|
20
|
+
To tackle the performance issue, I had the options of either implementing the C extension as part of ChunkyPNG, or build a separate library. <sup>[3]</sup> My initial gut feeling was to add a C extension to ChunkyPNG to give everyone a free performance boost. However, I soon discovered many people were using the library *because* it was pure Ruby. For me, it was a pragmatic implementation detail; for them, it was a feature.
|
21
|
+
|
22
|
+
Including a C extension would require everybody that wants to install ChunkyPNG to have a compiler toolchain installed. For me, installing a compiler toolchain is the first thing I do when I get a new machine. This is true for many Ruby developers, but it turns out that many of the library users are not Ruby developers at all. [Compass](http://compass-style.org/), a popular CSS authoring framework, uses ChunkyPNG to generate sprite images. Most Compass users are front-end developers who primarily use HTML, CSS and Javascript, and not Ruby. Because OS X comes with Ruby and Rubygems installed, running `gem install compass` works out of the box. Telling them to install a C compiler chain is simply an unacceptable installation requirement.
|
23
|
+
|
24
|
+
There are a couple of additional advantages of being a pure Ruby library. As an open source project ChunkyPNG can attract more contributors, because only a small percentage of Ruby developers are well-versed in C. Moreover, C extensions are MRI specific. This means that many C extensions won't work on Rubinius or JRuby, and I wanted my library to work in these environments as well. <sup>[4]</sup> Finally, libraries that require a C compiler inevitably get a lot of bug reports or support requests of people that are having issues installing the library, because of differences in development environments. <sup>[5]</sup>
|
25
|
+
|
26
|
+
### OilyPNG: a mixin library
|
27
|
+
|
28
|
+
So instead of adding a C extension, I started working on a separate library: [OilyPNG](https://github.com/wvanbergen/oily_png). Rather than making this a standalone library, I designed it to be a mixin module that depends on ChunkyPNG.
|
29
|
+
|
30
|
+
The approach is simple: OilyPNG consists of modules that implement some of the methods of ChunkyPNG in C. When OilyPNG is loaded with `require 'oily_png'`, it first loads ChunkyPNG and uses `Module#include` and `Module#extend` to [overwrite some methods in ChunkyPNG with OilyPNG's faster implementation](https://github.com/wvanbergen/oily_png/blob/master/lib/oily_png.rb).
|
31
|
+
|
32
|
+
This approach allows us to keep ChunkyPNG pure Ruby, and make OilyPNG 100% API compatible with ChunkyPNG. It is even possible to make OilyPNG optional in your project:
|
33
|
+
|
34
|
+
{% highlight ruby %}
|
35
|
+
begin
|
36
|
+
require 'oily_png'
|
37
|
+
rescue LoadError
|
38
|
+
require 'chunky_png'
|
39
|
+
end
|
40
|
+
{% endhighlight %}
|
41
|
+
|
42
|
+
This approach has some other advantages as well. Instead of having to implement everything at once to get to a library that implements most of ChunkyPNG, we can do this step by step while always providing 100% functional parity. Profile ChunkyPNG to find a slow method, implement it in OilyPNG, and iterate. This way OilyPNG doesn't suffer from a bootstrapping problem of having to implement and minimum viable subset of ChunkyPNG right from the start. It can grow organically, one optimized method at the time.
|
43
|
+
|
44
|
+
And because we have a well tested, pure Ruby implementation available to which OilyPNG is supposed to be 100% compatible, testing OilyPNG is simple. We just call a method on ChunkyPNG, run the exact same call on an OilyPNG-enhanced ChunkyPNG, and compare the results.
|
45
|
+
|
46
|
+
### To conclude
|
47
|
+
|
48
|
+
Being pure Ruby can be an important feature of a library for many of its users. Don't give it up too easily, even though Ruby's lacking performance may be an issue. Using a hybrid approach of a pure Ruby library with a native companion library is a great way to have the best of both worlds. <sup>[6]</sup>
|
49
|
+
|
50
|
+
---------------------------------------
|
51
|
+
|
52
|
+
#### Footnotes
|
53
|
+
|
54
|
+
1. This is also why I avoided using the [png gem](https://github.com/seattlerb/png), an "almost-pure-ruby" library that was available at the time. It uses [inline C](https://github.com/seattlerb/rubyinline) to speed up some of the algorithms.
|
55
|
+
2. Disclaimer: I should note that I haven't used ImageMagick and RMagick since 2010. So my knowledge about the current state of these libraries is extremely outdated at this point.
|
56
|
+
3. I could have leveraged the work of [libpng](http://www.libpng.org/pub/png/libpng.html) instead of implementing the algorithms myself. I decided not to, because libpng's API doesn't lend itself very well for the cherry-picking of hotspots approach I took with OilyPNG. You basically have to go all in if you want to use libpng. I think a Ruby PNG library that simply wraps libpng still has potential, but because of the reasons outlined in this article, I will leave that as an exercise to the reader. :)
|
57
|
+
4. Rubinius since has implemented most of MRI's C API so you can compile many C extensions against Rubinius as well, including OilyPNG. As an interesting side note: the Rubinius and JRuby developers have used ChunkyPNG as a performance benchmarking tool, because it contains a non-trivial amount of code and is computation heavy.
|
58
|
+
5. Unfortunately, OilyPNG is [not an exception](https://github.com/wvanbergen/oily_png/issues/12) to this rule.
|
59
|
+
6. My current employer — [Shopify](https://www.shopify.com) — is using the same approach for [Liquid](https://shopify.github.io/liquid/) and its C companion library [liquid-c](https://github.com/Shopify/liquid-c) with great success. Even though this requires matching Liquid's parsing behavior in certain edge cases quirk by quirk in the C implementation.
|
60
|
+
|
61
|
+
Thanks to Simon Hørup Eskildsen, Emilie Noël, and Steven H. Noble for reviewing drafts of this post.
|
data/docs/index.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
---
|
2
|
+
author: Willem van Bergen
|
3
|
+
title: ChunkyPNG
|
4
|
+
---
|
5
|
+
|
6
|
+
# ChunkyPNG
|
7
|
+
|
8
|
+
This library can read and write PNG files. It is written in pure Ruby for
|
9
|
+
maximum portability. Let me rephrase: it does NOT require RMagick or any other
|
10
|
+
memory leaking image library.
|
11
|
+
|
12
|
+
- Source code: [https://github.com/wvanbergen/chunky_png/tree/master](http://github.com/wvanbergen/chunky_png/tree/master)
|
13
|
+
- RDoc: [https://rdoc.info/gems/chunky_png](https://rdoc.info/gems/chunky_png)
|
14
|
+
- Wiki: [https://github.com/wvanbergen/chunky_png/wiki](https://github.com/wvanbergen/chunky_png/wiki)
|
15
|
+
- Issue tracker: [https://github.com/wvanbergen/chunky_png/issues](https://github.com/wvanbergen/chunky_png/issues)
|
16
|
+
|
17
|
+
## Features
|
18
|
+
|
19
|
+
- Decodes any image that the PNG standard allows. This includes all standard
|
20
|
+
color modes, all bit depths and all transparency, interlacing and filtering options.
|
21
|
+
- Encodes images supports all color modes (true color, grayscale and indexed)
|
22
|
+
and transparency for all these color modes. The best color mode will be
|
23
|
+
chosen automatically, based on the amount of used colors.
|
24
|
+
- R/W access to the image's pixels.
|
25
|
+
- R/W access to all image metadata that is stored in chunks.
|
26
|
+
- Memory efficient (uses a Fixnum, i.e. 4 or 8 bytes of memory per pixel, depending
|
27
|
+
on the hardware)
|
28
|
+
- Reasonably fast for Ruby standards, by only using integer math and a highly
|
29
|
+
optimized saving routine.
|
30
|
+
- Interoperability with RMagick if you really have to.
|
31
|
+
|
32
|
+
Also, have a look at [OilyPNG](http://github.com/wvanbergen/oily_png). OilyPNG is a
|
33
|
+
mixin module that implements some of the ChunkyPNG algorithms in C, which
|
34
|
+
provides a massive speed boost to encoding and decoding.
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
``` ruby
|
39
|
+
require 'chunky_png'
|
40
|
+
|
41
|
+
# Creating an image from scratch, save as an interlaced PNG
|
42
|
+
png = ChunkyPNG::Image.new(16, 16, ChunkyPNG::Color::TRANSPARENT)
|
43
|
+
png[1,1] = ChunkyPNG::Color.rgba(10, 20, 30, 128)
|
44
|
+
png[2,1] = ChunkyPNG::Color('black @ 0.5')
|
45
|
+
png.save('filename.png', :interlace => true)
|
46
|
+
|
47
|
+
# Compose images using alpha blending.
|
48
|
+
avatar = ChunkyPNG::Image.from_file('avatar.png')
|
49
|
+
badge = ChunkyPNG::Image.from_file('no_ie_badge.png')
|
50
|
+
avatar.compose!(badge, 10, 10)
|
51
|
+
avatar.save('composited.png', :fast_rgba) # Force the fast saving routine.
|
52
|
+
|
53
|
+
# Accessing metadata
|
54
|
+
image = ChunkyPNG::Image.from_file('with_metadata.png')
|
55
|
+
puts image.metadata['Title']
|
56
|
+
image.metadata['Author'] = 'Willem van Bergen'
|
57
|
+
image.save('with_metadata.png') # Overwrite file
|
58
|
+
|
59
|
+
# Low level access to PNG chunks
|
60
|
+
png_stream = ChunkyPNG::Datastream.from_file('filename.png')
|
61
|
+
png_stream.each_chunk { |chunk| p chunk.type }
|
62
|
+
```
|
63
|
+
|
64
|
+
For more information, see [the project wiki](http://github.com/wvanbergen/chunky_png/wiki)
|
65
|
+
or the RDOC documentation on [http://rdoc.info/gems/chunky_png](http://rdoc.info/gems/chunky_png)
|
66
|
+
|
67
|
+
## Security warning
|
68
|
+
|
69
|
+
ChunkyPNG is vulnerable to decompression bombs, which means that ChunkyPNG is vulnerable to
|
70
|
+
DOS attacks by running out of memory when loading a specifically crafted PNG file. Because
|
71
|
+
of the pure-Ruby nature of the library it is very hard to fix this problem in the library
|
72
|
+
itself.
|
73
|
+
|
74
|
+
In order to safely deal with untrusted images, you should make sure to do the image
|
75
|
+
processing using ChunkyPNG in a separate process, e.g. by using fork or a background
|
76
|
+
processing library.
|
77
|
+
|
78
|
+
## About
|
79
|
+
|
80
|
+
The library is written by Willem van Bergen for Floorplanner.com, and released
|
81
|
+
under the MIT license (see LICENSE). Please contact me for questions or
|
82
|
+
remarks. Patches are greatly appreciated!
|
83
|
+
|
84
|
+
Please check out the changelog on https://github.com/wvanbergen/chunky_png/wiki/Changelog
|
85
|
+
to see what changed in all versions.
|
86
|
+
|
87
|
+
P.S.: The name of this library is intentionally similar to Chunky Bacon and
|
88
|
+
Chunky GIF. Use Google if you want to know _why. :-)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
class Canvas
|
3
5
|
# The PNGDecoding contains methods for decoding PNG datastreams to create a
|
@@ -25,7 +27,7 @@ module ChunkyPNG
|
|
25
27
|
# combined to form the original images.
|
26
28
|
#
|
27
29
|
# @see ChunkyPNG::Canvas::PNGEncoding
|
28
|
-
# @see
|
30
|
+
# @see https://www.w3.org/TR/PNG/ The W3C PNG format specification
|
29
31
|
module PNGDecoding
|
30
32
|
# Decodes a Canvas from a PNG encoded string.
|
31
33
|
# @param [String] str The string to read from.
|
@@ -270,7 +272,7 @@ module ChunkyPNG
|
|
270
272
|
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
|
271
273
|
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
|
272
274
|
def decode_png_pixels_from_scanline_truecolor_8bit(stream, pos, width, _decoding_palette)
|
273
|
-
stream.unpack("@#{pos + 1}"
|
275
|
+
stream.unpack("@#{pos + 1}#{"NX" * width}").map { |c| c | 0x000000ff }
|
274
276
|
end
|
275
277
|
|
276
278
|
# Decodes a scanline of a 16-bit, true color image into a row of pixels.
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
class Canvas
|
3
5
|
# Methods for encoding a Canvas instance into a PNG datastream.
|
@@ -18,7 +20,7 @@ module ChunkyPNG
|
|
18
20
|
# before the compression step.
|
19
21
|
#
|
20
22
|
# @see ChunkyPNG::Canvas::PNGDecoding
|
21
|
-
# @see
|
23
|
+
# @see https://www.w3.org/TR/PNG/ The W3C PNG format specification
|
22
24
|
module PNGEncoding
|
23
25
|
# The palette used for encoding the image.This is only in used for images
|
24
26
|
# that get encoded using indexed colors.
|
@@ -171,7 +173,7 @@ module ChunkyPNG
|
|
171
173
|
# @param [Integer] filtering The filtering method to use.
|
172
174
|
# @return [String] The PNG encoded canvas as string.
|
173
175
|
def encode_png_image_without_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE)
|
174
|
-
stream =
|
176
|
+
stream = "".b
|
175
177
|
encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering)
|
176
178
|
stream
|
177
179
|
end
|
@@ -187,7 +189,7 @@ module ChunkyPNG
|
|
187
189
|
# @param [Integer] filtering The filtering method to use.
|
188
190
|
# @return [String] The PNG encoded canvas as string.
|
189
191
|
def encode_png_image_with_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE)
|
190
|
-
stream =
|
192
|
+
stream = "".b
|
191
193
|
0.upto(6) do |pass|
|
192
194
|
subcanvas = self.class.adam7_extract_pass(pass, self)
|
193
195
|
subcanvas.encoding_palette = encoding_palette
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
class Canvas
|
3
5
|
# Methods to quickly load a canvas from a stream, encoded in RGB, RGBA, BGR or ABGR format.
|
@@ -51,7 +53,7 @@ module ChunkyPNG
|
|
51
53
|
def from_bgr_stream(width, height, stream)
|
52
54
|
string = ChunkyPNG::EXTRA_BYTE.dup # Add a first byte to the first BGR triple.
|
53
55
|
string << (stream.respond_to?(:read) ? stream.read(3 * width * height) : stream.to_s[0, 3 * width * height])
|
54
|
-
pixels = string.unpack("@1"
|
56
|
+
pixels = string.unpack("@1#{"XV" * (width * height)}").map { |color| color | 0x000000ff }
|
55
57
|
new(width, height, pixels)
|
56
58
|
end
|
57
59
|
|
data/lib/chunky_png/canvas.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
require "chunky_png/canvas/png_encoding"
|
2
4
|
require "chunky_png/canvas/png_decoding"
|
3
5
|
require "chunky_png/canvas/adam7_interlacing"
|
@@ -295,7 +297,7 @@ module ChunkyPNG
|
|
295
297
|
# @return [String] A nicely formatted string representation of this canvas.
|
296
298
|
# @private
|
297
299
|
def inspect
|
298
|
-
inspected = "<#{self.class.name} #{width}x#{height} ["
|
300
|
+
inspected = +"<#{self.class.name} #{width}x#{height} ["
|
299
301
|
for y in 0...height
|
300
302
|
inspected << "\n\t[" << row(y).map { |p| ChunkyPNG::Color.to_hex(p) }.join(" ") << "]"
|
301
303
|
end
|
data/lib/chunky_png/chunk.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
# A PNG datastream consists of multiple chunks. This module, and the classes
|
3
5
|
# contained within, help with handling these chunks. It supports both reading
|
@@ -117,6 +119,8 @@ module ChunkyPNG
|
|
117
119
|
# PNG spec, except for color depth: Only 8-bit depth images are supported.
|
118
120
|
# Note that it is still possible to access the chunk for such an image, but
|
119
121
|
# ChunkyPNG will raise an exception if you try to access the pixel data.
|
122
|
+
#
|
123
|
+
# @see https://www.w3.org/TR/PNG/#11IHDR
|
120
124
|
class Header < Base
|
121
125
|
attr_accessor :width, :height, :depth, :color, :compression, :filtering, :interlace
|
122
126
|
|
@@ -166,6 +170,8 @@ module ChunkyPNG
|
|
166
170
|
|
167
171
|
# The End (IEND) chunk indicates the last chunk of a PNG stream. It does
|
168
172
|
# not contain any data.
|
173
|
+
#
|
174
|
+
# @see https://www.w3.org/TR/PNG/#11IEND
|
169
175
|
class End < Base
|
170
176
|
def initialize
|
171
177
|
super("IEND")
|
@@ -186,13 +192,14 @@ module ChunkyPNG
|
|
186
192
|
# Returns an empty string, because this chunk should always be empty.
|
187
193
|
# @return [""] An empty string.
|
188
194
|
def content
|
189
|
-
|
195
|
+
"".b
|
190
196
|
end
|
191
197
|
end
|
192
198
|
|
193
199
|
# The Palette (PLTE) chunk contains the image's palette, i.e. the
|
194
200
|
# 8-bit RGB colors this image is using.
|
195
201
|
#
|
202
|
+
# @see https://www.w3.org/TR/PNG/#11PLTE
|
196
203
|
# @see ChunkyPNG::Chunk::Transparency
|
197
204
|
# @see ChunkyPNG::Palette
|
198
205
|
class Palette < Generic
|
@@ -210,6 +217,7 @@ module ChunkyPNG
|
|
210
217
|
# Images having a color mode that already includes an alpha channel, this
|
211
218
|
# chunk should not be included.
|
212
219
|
#
|
220
|
+
# @see https://www.w3.org/TR/PNG/#11tRNS
|
213
221
|
# @see ChunkyPNG::Chunk::Palette
|
214
222
|
# @see ChunkyPNG::Palette
|
215
223
|
class Transparency < Generic
|
@@ -249,7 +257,18 @@ module ChunkyPNG
|
|
249
257
|
end
|
250
258
|
end
|
251
259
|
|
260
|
+
# An image data (IDAT) chunk holds (part of) the compressed image pixel data.
|
261
|
+
#
|
262
|
+
# The data of an image can be split over multiple chunks, which will have to be combined
|
263
|
+
# and inflated in order to decode an image. See {{.combine_chunks}} to combine chunks
|
264
|
+
# to decode, and {{.split_in_chunks}} for encoding a pixeldata stream into IDAT chunks.
|
265
|
+
#
|
266
|
+
# @see https://www.w3.org/TR/PNG/#11IDAT
|
252
267
|
class ImageData < Generic
|
268
|
+
# Combines the list of IDAT chunks and inflates their contents to produce the
|
269
|
+
# pixeldata stream for the image.
|
270
|
+
#
|
271
|
+
# @return [String] The combined, inflated pixeldata as binary string
|
253
272
|
def self.combine_chunks(data_chunks)
|
254
273
|
zstream = Zlib::Inflate.new
|
255
274
|
data_chunks.each { |c| zstream << c.content }
|
@@ -258,6 +277,12 @@ module ChunkyPNG
|
|
258
277
|
inflated
|
259
278
|
end
|
260
279
|
|
280
|
+
# Splits and compresses a pixeldata stream into a list of IDAT chunks.
|
281
|
+
#
|
282
|
+
# @param data [String] The binary string of pixeldata
|
283
|
+
# @param level [Integer] The compression level to use.
|
284
|
+
# @param chunk_size [Integer] The maximum size of a chunk.
|
285
|
+
# @return Array<ChunkyPNG::Chunk::ImageData> The list of IDAT chunks.
|
261
286
|
def self.split_in_chunks(data, level = Zlib::DEFAULT_COMPRESSION, chunk_size = 2147483647)
|
262
287
|
streamdata = Zlib::Deflate.deflate(data, level)
|
263
288
|
# TODO: Split long streamdata over multiple chunks
|
@@ -266,11 +291,12 @@ module ChunkyPNG
|
|
266
291
|
end
|
267
292
|
|
268
293
|
# The Text (tEXt) chunk contains keyword/value metadata about the PNG
|
269
|
-
# stream.
|
294
|
+
# stream. In this chunk, the value is stored uncompressed.
|
270
295
|
#
|
271
296
|
# The tEXt chunk only supports Latin-1 encoded textual data. If you need
|
272
297
|
# UTF-8 support, check out the InternationalText chunk type.
|
273
298
|
#
|
299
|
+
# @see https://www.w3.org/TR/PNG/#11tEXt
|
274
300
|
# @see ChunkyPNG::Chunk::CompressedText
|
275
301
|
# @see ChunkyPNG::Chunk::InternationalText
|
276
302
|
class Text < Base
|
@@ -299,6 +325,7 @@ module ChunkyPNG
|
|
299
325
|
# PNG stream. In this chunk, the value is compressed using Deflate
|
300
326
|
# compression.
|
301
327
|
#
|
328
|
+
# @see https://www.w3.org/TR/PNG/#11zTXt
|
302
329
|
# @see ChunkyPNG::Chunk::CompressedText
|
303
330
|
# @see ChunkyPNG::Chunk::InternationalText
|
304
331
|
class CompressedText < Base
|
@@ -331,7 +358,7 @@ module ChunkyPNG
|
|
331
358
|
# The Physical (pHYs) chunk specifies the intended pixel size or aspect
|
332
359
|
# ratio for display of the image.
|
333
360
|
#
|
334
|
-
#
|
361
|
+
# @see https://www.w3.org/TR/PNG/#11pHYs
|
335
362
|
class Physical < Base
|
336
363
|
attr_accessor :ppux, :ppuy, :unit
|
337
364
|
|
@@ -374,8 +401,7 @@ module ChunkyPNG
|
|
374
401
|
# a translation of the keyword name. Finally, it supports bot compressed
|
375
402
|
# and uncompressed values.
|
376
403
|
#
|
377
|
-
#
|
378
|
-
#
|
404
|
+
# @see https://www.w3.org/TR/PNG/#11iTXt
|
379
405
|
# @see ChunkyPNG::Chunk::Text
|
380
406
|
# @see ChunkyPNG::Chunk::CompressedText
|
381
407
|
class InternationalText < Base
|
@@ -391,7 +417,7 @@ module ChunkyPNG
|
|
391
417
|
@compression = compression
|
392
418
|
end
|
393
419
|
|
394
|
-
# Reads the
|
420
|
+
# Reads the iTXt chunk.
|
395
421
|
# @param type [String] The four character chunk type indicator (= "iTXt").
|
396
422
|
# @param content [String] The content read from the chunk.
|
397
423
|
# @return [ChunkyPNG::Chunk::InternationalText] The new End chunk instance.
|
data/lib/chunky_png/color.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
# Factory method to return a color value, based on the arguments given.
|
3
5
|
#
|
@@ -188,7 +190,7 @@ module ChunkyPNG
|
|
188
190
|
# @param [Fixnum] alpha Defaults to opaque (255).
|
189
191
|
# @return [Integer] The newly constructed color value.
|
190
192
|
# @raise [ArgumentError] if the hsv triple is invalid.
|
191
|
-
# @see
|
193
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
192
194
|
def from_hsv(hue, saturation, value, alpha = 255)
|
193
195
|
raise ArgumentError, "Hue must be between 0 and 360" unless (0..360).cover?(hue)
|
194
196
|
raise ArgumentError, "Saturation must be between 0 and 1" unless (0..1).cover?(saturation)
|
@@ -214,7 +216,7 @@ module ChunkyPNG
|
|
214
216
|
# @param [Fixnum] alpha Defaults to opaque (255).
|
215
217
|
# @return [Integer] The newly constructed color value.
|
216
218
|
# @raise [ArgumentError] if the hsl triple is invalid.
|
217
|
-
# @see
|
219
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
218
220
|
def from_hsl(hue, saturation, lightness, alpha = 255)
|
219
221
|
raise ArgumentError, "Hue #{hue} was not between 0 and 360" unless (0..360).cover?(hue)
|
220
222
|
raise ArgumentError, "Saturation #{saturation} was not between 0 and 1" unless (0..1).cover?(saturation)
|
@@ -245,8 +247,7 @@ module ChunkyPNG
|
|
245
247
|
# @param [Fixnum] chroma The associated chroma value.
|
246
248
|
# @return [Array<Fixnum>] A scaled r,g,b triple. Scheme-dependent
|
247
249
|
# adjustments are still needed to reach the true r,g,b values.
|
248
|
-
# @see
|
249
|
-
# @see http://www.tomjewett.com/colors/hsb.html
|
250
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
250
251
|
# @private
|
251
252
|
def cylindrical_to_cubic(hue, saturation, y_component, chroma)
|
252
253
|
hue_prime = hue.fdiv(60)
|
@@ -590,7 +591,7 @@ module ChunkyPNG
|
|
590
591
|
# @return [Array[2]] The value of the color (0-1)
|
591
592
|
# @return [Array[3]] Optional fourth element for alpha, included if
|
592
593
|
# include_alpha=true (0-255)
|
593
|
-
# @see
|
594
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
594
595
|
def to_hsv(color, include_alpha = false)
|
595
596
|
hue, chroma, max, _ = hue_and_chroma(color)
|
596
597
|
value = max
|
@@ -619,7 +620,7 @@ module ChunkyPNG
|
|
619
620
|
# @return [Array<Fixnum>[2]] The lightness of the color (0-1)
|
620
621
|
# @return [Array<Fixnum>[3]] Optional fourth element for alpha, included if
|
621
622
|
# include_alpha=true (0-255)
|
622
|
-
# @see
|
623
|
+
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
|
623
624
|
def to_hsl(color, include_alpha = false)
|
624
625
|
hue, chroma, max, min = hue_and_chroma(color)
|
625
626
|
lightness = 0.5 * (max + min)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
# The Datastream class represents a PNG formatted datastream. It supports
|
3
5
|
# both reading from and writing to strings, streams and files.
|
@@ -9,7 +11,7 @@ module ChunkyPNG
|
|
9
11
|
# @see ChunkyPNG::Chunk
|
10
12
|
class Datastream
|
11
13
|
# The signature that each PNG file or stream should begin with.
|
12
|
-
SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack("C8").force_encoding(Encoding::BINARY).freeze
|
14
|
+
SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack("C8").force_encoding(::Encoding::BINARY).freeze
|
13
15
|
|
14
16
|
# The header chunk of this datastream.
|
15
17
|
# @return [ChunkyPNG::Chunk::Header]
|
@@ -72,7 +74,7 @@ module ChunkyPNG
|
|
72
74
|
# @param [IO] io The stream to read from.
|
73
75
|
# @return [ChunkyPNG::Datastream] The loaded datastream instance.
|
74
76
|
def from_io(io)
|
75
|
-
io.set_encoding(Encoding::BINARY)
|
77
|
+
io.set_encoding(::Encoding::BINARY)
|
76
78
|
verify_signature!(io)
|
77
79
|
|
78
80
|
ds = new
|
@@ -156,12 +158,6 @@ module ChunkyPNG
|
|
156
158
|
# WRITING DATASTREAMS
|
157
159
|
##################################################################################
|
158
160
|
|
159
|
-
# Returns an empty stream using binary encoding that can be used as stream to encode to.
|
160
|
-
# @return [String] An empty, binary string.
|
161
|
-
def self.empty_bytearray
|
162
|
-
ChunkyPNG::EMPTY_BYTEARRAY.dup
|
163
|
-
end
|
164
|
-
|
165
161
|
# Writes the datastream to the given output stream.
|
166
162
|
# @param [IO] io The output stream to write to.
|
167
163
|
def write(io)
|
data/lib/chunky_png/dimension.rb
CHANGED
data/lib/chunky_png/image.rb
CHANGED
data/lib/chunky_png/palette.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
module ChunkyPNG
|
2
4
|
# A palette describes the set of colors that is being used for an image.
|
3
5
|
#
|
@@ -10,7 +12,7 @@ module ChunkyPNG
|
|
10
12
|
# explicit palette (stores as PLTE & tRNS chunks in a PNG file).
|
11
13
|
#
|
12
14
|
# @see ChunkyPNG::Color
|
13
|
-
class Palette <
|
15
|
+
class Palette < Set
|
14
16
|
# Builds a new palette given a set (Enumerable instance) of colors.
|
15
17
|
#
|
16
18
|
# @param enum [Enumerable<Integer>] The set of colors to include in this
|
@@ -19,8 +21,10 @@ module ChunkyPNG
|
|
19
21
|
# which they appeared in the palette chunk, so that this array can be
|
20
22
|
# used for decoding.
|
21
23
|
def initialize(enum, decoding_map = nil)
|
22
|
-
super(enum)
|
24
|
+
super(enum.sort.freeze)
|
23
25
|
@decoding_map = decoding_map if decoding_map
|
26
|
+
@encoding_map = {}
|
27
|
+
freeze
|
24
28
|
end
|
25
29
|
|
26
30
|
# Builds a palette instance from a PLTE chunk and optionally a tRNS chunk
|
@@ -133,7 +137,7 @@ module ChunkyPNG
|
|
133
137
|
# @return [true, false] True if a encoding map was built when this palette
|
134
138
|
# was loaded.
|
135
139
|
def can_encode?
|
136
|
-
!@encoding_map.
|
140
|
+
!@encoding_map.empty?
|
137
141
|
end
|
138
142
|
|
139
143
|
# Returns a color, given the position in the original palette chunk.
|
@@ -174,8 +178,8 @@ module ChunkyPNG
|
|
174
178
|
# @return [ChunkyPNG::Chunk::Palette] The PLTE chunk.
|
175
179
|
# @see ChunkyPNG::Palette#can_encode?
|
176
180
|
def to_plte_chunk
|
177
|
-
@encoding_map
|
178
|
-
colors
|
181
|
+
@encoding_map.clear
|
182
|
+
colors = []
|
179
183
|
|
180
184
|
each_with_index do |color, index|
|
181
185
|
@encoding_map[color] = index
|
data/lib/chunky_png/point.rb
CHANGED
data/lib/chunky_png/rmagick.rb
CHANGED
data/lib/chunky_png/vector.rb
CHANGED
data/lib/chunky_png/version.rb
CHANGED
data/lib/chunky_png.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
1
3
|
# Basic requirements from standard library
|
2
4
|
require "set"
|
3
5
|
require "zlib"
|
@@ -138,16 +140,10 @@ module ChunkyPNG
|
|
138
140
|
class UnitsUnknown < ChunkyPNG::Exception
|
139
141
|
end
|
140
142
|
|
141
|
-
# Empty byte array. This basically is an empty string, but with the encoding
|
142
|
-
# set correctly to ASCII-8BIT (binary) in Ruby 1.9.
|
143
|
-
# @return [String] An empty string, with encoding set to binary in Ruby 1.9
|
144
|
-
# @private
|
145
|
-
EMPTY_BYTEARRAY = "".force_encoding(Encoding::BINARY).freeze
|
146
|
-
|
147
143
|
# Null-byte, with the encoding set correctly to ASCII-8BIT (binary) in Ruby 1.9.
|
148
144
|
# @return [String] A binary string, consisting of one NULL-byte.
|
149
145
|
# @private
|
150
|
-
EXTRA_BYTE = "\0".
|
146
|
+
EXTRA_BYTE = "\0".b
|
151
147
|
end
|
152
148
|
|
153
149
|
require "chunky_png/version"
|
@@ -231,4 +231,10 @@ describe ChunkyPNG::Canvas do
|
|
231
231
|
expect(subject.send(:replace_canvas!, 2, 2, [1, 2, 3, 4])).to equal(subject)
|
232
232
|
end
|
233
233
|
end
|
234
|
+
|
235
|
+
describe "#inspect" do
|
236
|
+
it "should give a string description of the canvas" do
|
237
|
+
expect(subject.inspect).to eql "<ChunkyPNG::Canvas 1x1 [\n\t[#ffffffff]\n]>"
|
238
|
+
end
|
239
|
+
end
|
234
240
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: chunky_png
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -80,7 +80,7 @@ description: |2
|
|
80
80
|
alpha composition and cropping. Finally, it can import from and export to RMagick for
|
81
81
|
interoperability.
|
82
82
|
|
83
|
-
Also, have a look at OilyPNG at
|
83
|
+
Also, have a look at OilyPNG at https://github.com/wvanbergen/oily_png. OilyPNG is a
|
84
84
|
drop in mixin module that implements some of the ChunkyPNG algorithms in C, which
|
85
85
|
provides a massive speed boost to encoding and decoding.
|
86
86
|
email:
|
@@ -93,9 +93,9 @@ extra_rdoc_files:
|
|
93
93
|
- CONTRIBUTING.rdoc
|
94
94
|
- CHANGELOG.rdoc
|
95
95
|
files:
|
96
|
+
- ".github/workflows/ruby.yml"
|
96
97
|
- ".gitignore"
|
97
98
|
- ".standard.yml"
|
98
|
-
- ".travis.yml"
|
99
99
|
- ".yardopts"
|
100
100
|
- BENCHMARKING.rdoc
|
101
101
|
- CHANGELOG.rdoc
|
@@ -110,6 +110,13 @@ files:
|
|
110
110
|
- bin/rake
|
111
111
|
- bin/standardrb
|
112
112
|
- chunky_png.gemspec
|
113
|
+
- docs/.gitignore
|
114
|
+
- docs/CNAME
|
115
|
+
- docs/_config.yml
|
116
|
+
- docs/_posts/2010-01-14-memory-efficiency-when-using-ruby.md
|
117
|
+
- docs/_posts/2010-01-17-ode-to-array-pack-and-string-unpack.md
|
118
|
+
- docs/_posts/2014-11-07-the-value-of-a-pure-ruby-library.md
|
119
|
+
- docs/index.md
|
113
120
|
- lib/chunky_png.rb
|
114
121
|
- lib/chunky_png/canvas.rb
|
115
122
|
- lib/chunky_png/canvas/adam7_interlacing.rb
|
@@ -425,7 +432,7 @@ licenses:
|
|
425
432
|
metadata:
|
426
433
|
source_code_uri: https://github.com/wvanbergen/chunky_png
|
427
434
|
wiki_uri: https://github.com/wvanbergen/chunky_png/wiki
|
428
|
-
post_install_message:
|
435
|
+
post_install_message:
|
429
436
|
rdoc_options:
|
430
437
|
- "--title"
|
431
438
|
- chunky_png
|
@@ -439,7 +446,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
439
446
|
requirements:
|
440
447
|
- - ">="
|
441
448
|
- !ruby/object:Gem::Version
|
442
|
-
version: '0'
|
449
|
+
version: '2.0'
|
443
450
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
444
451
|
requirements:
|
445
452
|
- - ">="
|
@@ -447,7 +454,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
447
454
|
version: '0'
|
448
455
|
requirements: []
|
449
456
|
rubygems_version: 3.0.3
|
450
|
-
signing_key:
|
457
|
+
signing_key:
|
451
458
|
specification_version: 4
|
452
459
|
summary: Pure ruby library for read/write, chunk-level access to PNG files
|
453
460
|
test_files:
|