chunky_png-1.3.15/ 0000755 0001750 0001750 00000000000 13766004353 013227 5 ustar daniel daniel chunky_png-1.3.15/Rakefile 0000644 0001750 0001750 00000000430 13766004353 014671 0 ustar daniel daniel # frozen-string-literal: true
require "bundler/gem_tasks"
require "rspec/core/rake_task"
Dir["tasks/*.rake"].each { |file| load(file) }
RSpec::Core::RakeTask.new(:spec) do |task|
task.pattern = "./spec/**/*_spec.rb"
task.rspec_opts = ["--color"]
end
task default: [:spec]
chunky_png-1.3.15/chunky_png.gemspec 0000644 0001750 0001750 00000004435 13766004353 016747 0 ustar daniel daniel # frozen-string-literal: true
lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "chunky_png/version"
Gem::Specification.new do |s|
s.name = "chunky_png"
# Do not change the version and date fields by hand. This will be done
# automatically by the gem release script.
s.version = ChunkyPNG::VERSION
s.summary = "Pure ruby library for read/write, chunk-level access to PNG files"
s.description = <<-EOT
This pure Ruby library can read and write PNG images without depending on an external
image library, like RMagick. It tries to be memory efficient and reasonably fast.
It supports reading and writing all PNG variants that are defined in the specification,
with one limitation: only 8-bit color depth is supported. It supports all transparency,
interlacing and filtering options the PNG specifications allows. It can also read and
write textual metadata from PNG files. Low-level read/write access to PNG chunks is
also possible.
This library supports simple drawing on the image canvas and simple operations like
alpha composition and cropping. Finally, it can import from and export to RMagick for
interoperability.
Also, have a look at OilyPNG at https://github.com/wvanbergen/oily_png. OilyPNG is a
drop in mixin module that implements some of the ChunkyPNG algorithms in C, which
provides a massive speed boost to encoding and decoding.
EOT
s.authors = ["Willem van Bergen"]
s.email = ["willem@railsdoctors.com"]
s.homepage = "https://github.com/wvanbergen/chunky_png/wiki"
s.license = "MIT"
s.metadata = {
"source_code_uri" => "https://github.com/wvanbergen/chunky_png",
"wiki_uri" => "https://github.com/wvanbergen/chunky_png/wiki",
}
s.add_development_dependency("rake")
s.add_development_dependency("standard")
s.add_development_dependency("yard", "~> 0.9")
s.add_development_dependency("rspec", "~> 3")
s.rdoc_options << "--title" << s.name << "--main" << "README.rdoc" << "--line-numbers" << "--inline-source"
s.extra_rdoc_files = ["README.md", "BENCHMARKING.rdoc", "CONTRIBUTING.rdoc", "CHANGELOG.rdoc"]
s.files = `git ls-files`.split($/)
s.test_files = s.files.grep(%r{^(test|spec|features)/})
s.required_ruby_version = ">= 2.0.0"
end
chunky_png-1.3.15/CHANGELOG.rdoc 0000644 0001750 0001750 00000023457 13766004353 015402 0 ustar daniel daniel = Changelog
The file documents the changes to this library over the different versions.
- ChunkyPNG uses semantic versioning. This means that the public API will not change except for major versions.
- Please add an entry to the "Unreleased changes" section in your pull requests, so I can move them into a numbered version section on release.
=== Unreleased changes
- Implemented ChunkyPNG::Dimension#hash to fix some specs after a behavior change in RSpec.
=== 1.3.11 - 2018-11-21
- Updated project metadata as published on Rubygems.org
=== 1.3.10 - 2018-01-23
- Fixed a regression in Datastream#metadata, which was not able to deal with iTXt chunks.
=== 1.3.9 - 2018-01-23
- Add support for reading and writing an international textual data (iTXt chunks).
=== 1.3.8 - 2016-08-31
- Add support for reading and writing an image's physical dimension (pHYs chunks).
=== 1.3.7 - 2016-08-31
- Performance improvement for Color.euclidean_distance_rgba.
- Bugfix in decoding transparent pixels when decoding multiple images in a row.
=== 1.3.6 - 2016-06-19
- Allow reading images from streams that have trailing data after the IEND chunk.
- Add compatibility for Ruby 2.3's frozen string literals.
- Documentation updates and small cleanups.
=== 1.3.5 - 2015-10-28
- Performance improvements for Canvas#crop! and ImageData.combine_chunks
- Update chunky_png/rmagick to work with the latest versions of RMagick.
- Bugfix in Color#from_hsl and Color#from_hsv when hue value is 360.
- Fix encoding issue in Datastream#to_blob
=== 1.3.4 - 2015-02-16
- Assert compatibility with Ruby 2.2
- Improved documentation using RDoc, so it is included on https://www.rubydoc.info/gems/chunky_png
- Update chunkypng.com website; migrate some stuff from the wiki.
=== 1.3.3 - 2014-10-24
- Improve performance of Canvas#crop and Canvas#crop! by doing less memory allocations.
- Update to RSPEC 3
- Add CONTRIBUTING.rdoc file.
=== 1.3.2 - 2014-10-18
- Add HSV/HSL color conversions: Color.from_hsl, Color.to_hsl
- Allow empty IDAT chunks to better conform to the PNG standard.
- Small bugfix in image resampling.
- Documentation and code readability improvements.
=== 1.3.1 - 2014-04-28
- Improve performance of Palette.from_canvas.
- Add Color.euclidean_distance_rgba to compare colors.
- Bugix in Canvas.from_bgr_stream.
- Documentation and code readibility improvements.
- README updates, include mention of screencast.
=== 1.3.0 - 2014-02-10
- Add support for parsing color that use three-hex notation. (e.g. #aaa instead of #aaaaaa)
- Add Canvas#border! and Canvas#border to draw a border around a canvas.
- Add Canvas#trim! and Canvas#trim to trim a border from a canvas.
=== 1.2.9 - 2013-10-17
- Set license in chunky_png.gemspec for better discoverability.
- Improve error messages for Canvas#crop.
- Use better gem release management tasks from bundler.
=== 1.2.8 - 2013-03-30
- Ruby 2.0 compatibility.
- Fixed some encoding issues on JRuby.
- Update Travis CI configuration to test on more Ruby versions.
=== 1.2.7 - 2013-01-07
- Small PNG decoding performnace improvements by using bitwise math.
=== 1.2.6 - 2012-08-07
- Add decompression bomb security warning to README.
- Fix RMagick loading issue on case sensitive filesystems.
- Some compatibility fixes for the upcoming Ruby 2.0.
- Allow more data-url notations for ChunkyPNG::Canvas.from_data_url.
=== 1.2.5 - 2011-09-23
- Edge case bugfix in Color.decompose_alpha_component that could get triggered in the change_theme_color! method.
=== 1.2.4 - 2011-09-14
- Added data URL importing Canvas.from_data_url.
=== 1.2.3 - 2011-09-14
- Added data URL exporting Canvas#to_data_url to easily use PNGs inline in CSS or HTML.
=== 1.2.2 - 2011-09-14
- Workaround for performance bug in REE.
=== 1.2.1 - 2011-08-10
- Added bicubic resampling of images.
- Update resampling code to use integer math instead of floating points.
=== 1.2.0 - 2011-05-08
- Properly read PNG files with a tRNS chunk in color mode 0 (grayscale) or 2 (true color).
=== 1.1.2 - 2011-05-06
- Added Color.to_grayscale and Canvas#grayscale! to convert colors and canvases to grayscale.
- Memory footprint improvement of Canvas#resample!
=== 1.1.1 - 2011-04-22
- Added Canvas#to_alpha_channel_bytes and Canvas#to_grayscale_stream to export raw pixel data.
- Spec suite cleanup
=== 1.1.0 - 2011-03-19
- Add bezier curve drawing: Canvas#bezier_curve.
- RDoc fixes & improvements.
=== 1.0.1 - 2011-03-08
- Performance improvements.
=== 1.0.0 - 2011-03-06
There are some API changes for this release. If you are using Canvas#compose or Canvas#replace, these methods will no longer operate in place, but will return a new canvas instance instead. The in place versions have been renamed to compose! and replace! to be more consistent with the rest of the API.
- Added image resampling using the nearest neighbor algorithm: Canvas#resample.
- Added circle and polygon drawing methods: Canvas#circle and Canvas#polygon.
- Added in place version of Canvas#crop, Canvas#rotate_180, Canvas#flip_horizontally and Canvas#flip_vertically. Just add a bang to the method name (e.g. Canvas#crop!) and it will change the current canvas instead of returning a new one. These implementations are also more memory and CPU efficient.
- Added geometry helper classes: ChunkyPNG::Point, ChunkyPNG::Dimension and ChunkyPNG::Vector.
- Added a list of HTML named colors. Get them by calling ChunkyPNG::Color(:teal) or ChunkyPNG::Color('red @ 0.8')
- Added encoding support for 1-, 2-, and 4-bit grayscale images.
- 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.)
- RDoc improvements. See https://rdoc.info/gems/chunky_png.
- ChunkyPNG is now also tested on Ruby 1.8.6.
=== 0.12.0 - 2010-12-12
- Added support for encoding indexed images with a low bitrate. It will automatically use less bits per pixel if possible.
- Improved testing setup. ChunkyPNG is now tested on Ruby 1.8.7, 1.9.2, JRuby and Rubinius.
=== 0.11.0 - 2010-11-16
- Decoding of 1, 2 and 4 bit indexed color images.
- Decoding of 1, 2 and 4 bit grayscale images.
- Decoding 16 bit images. The extra bits will be discarded, so the image will be loaded as 8 bit.
- Used the official PNG suite to build a more complete test suite.
=== 0.10.5 - 2010-10-21
- Bugfix: allow 256 instead of 255 colors for indexed images.
=== 0.10.4 - 2010-10-17
- Improved handling of binary encoding for strings in Ruby 1.9.
=== 0.10.3 - 2010-10-07
- Small fix to make grayscale use the B byte consistently.
=== 0.10.2 - 2010-10-04
- Another small fix for OilyPNG compatibility
=== 0.10.1 - 2010-10-03
- Small fix for OilyPNG compatibility
=== 0.10.0 - 2010-10-03
- Refactored decoding and encoding to work on binary strings instead of arrays of integers. This gives a nice speedup and uses less memory. Thanks to Yehuda Katz for the idea.
=== 0.9.2 - 2010-09-16
- Fixed an issue with interlaced images.
=== 0.9.1 - 2010-09-15
- Fixed image metadata issue when duplicating images.
=== 0.9.0 - 2010-08-18
- Added flip_horizontally, flip_vertically, rotate_left, rotate_right and rotate_180 to ChunkyPNG::Canvas.
- Now raises ChunkyPNG::OutOfBounds exceptions when referencing coordinates outside the image bounds.
- Added Gemfile for development dependency management.
=== 0.8.0 - 2010-06-30
- Added ChunkyPNG::Image#rect to draw simple rectangles.
- Fixed composing a transparent color on a fully transparent background.
=== 0.7.3 - 2010-04-28
- Based on the suggestion of [Dirkjan Bussink](https://github.com/dbussink), introduced custom exception classes:
- ChunkyPNG::SignatureMismatch is raised when the PNG signature could not be found. Usually this means the the file is not a PNG image.
- ChunkyPNG::CRCMismatch is raised when the a CRC check for a chunk in the PNG file fails.
- ChunkyPNG::NotSupported is raised when the PNG image uses a feature that ChunkyPNG does not support.
- ChunkyPNG::ExpectationFailed is raised when a required expectation failed.
=== 0.7.2 - 2010-04-28 [YANKED]
=== 0.7.1 - 2010-03-23
- Some fixes for 32-bit systems.
=== 0.7.0 - 2010-03-15
- Added :best_compression saving routine to allow creating the smallest images possible.
- Added option to control Zlib compression level while saving.
=== 0.6.0 - 2010-02-25
- Added methods to easily create different color variants of an image with a color theme. See [[Images with a color theme]] for more information.
=== 0.5.8 - 2010-02-24
- Ruby 1.8.6 compatibility fixes
- Improved API documentation.
=== 0.5.5 - 2010-02-15
- Added alpha decomposition to extract a color mask from a themed image.
- Improved API documentation.
=== 0.5.4 - 2010-01-17
- Added point and line anti-aliased drawing functions.
=== 0.5.3 - 2010-01-16
- Removed last occurrences of floating math to speed up the library.
- Added importing of ABGR and BGR streams.
- Added exporting an image as ABGR stream.
=== 0.5.2 - 2010-01-15
- Ruby 1.9 compatibility fixes.
- Improved speed of PNG decoding.
- Bugfix in *average* scanline decoding filter.
=== 0.5.1 - 2010-01-15
- Added :fast_rgba and :fast_rgb saving routines, which yield a 1500% speedup when saving an image.
=== 0.5.0 - 2010-01-15
- Complete rewrite of the earlier versions, now including awesomeness and unicorns.
chunky_png-1.3.15/BENCHMARKING.rdoc 0000644 0001750 0001750 00000003305 13766004353 015731 0 ustar daniel daniel = ChunkyPNG benchmark suite
I would like the performance of this library as good as possible, and I will
gladly accept changes to this library that improves performance.
The library comes with a basic benchmark suite is intended to test the speed
of PNG decoding and encoding against different ruby interpreters. Execute them
using rake. You can set the number of runs by passing the N environment variable.
bundle exec rake benchmark:encoding
bundle exec rake benchmark:decoding
bundle exec rake benchmark N=10 # Run all of them with 10 iterations
You can use rvm to run the benchmarks against different interpreters. Of course,
make sure that the chunky_png is installed for all your interpreters.
rvm 1.8.7,1.9.3,rbx bundle exec rake benchmark N=10
== Results
What is a speed improvement on one interpreter doesn't necessarily mean the
performance will be better on other interpreters as well. Please make sure to
benchamrk different RUby interpreters. When it comes to different Ruby
interpreters, the priority is the performance on recent MRI versions.
Some very old benchmark result (using N=50) on my 2007 iMac can be
found at https://gist.github.com/wvanbergen/495323.
== Why is this relevant?
ChunkyPNG is a pure Ruby library to handle PNG files. Decoding a PNG requires
a lot of integer math and bitwise operations, and moderate use of the unpack
method to read binary data. Encoding is a good test for +Array#pack+, and
depending on the encoding options, also requires a lot of calculations.
Therefore, the library is a good benchmark candidate for these methods and
algorithms. It has been used to improve the Array#pack and
String#unpack methods in Rubinius.
chunky_png-1.3.15/lib/ 0000755 0001750 0001750 00000000000 13766004353 013775 5 ustar daniel daniel chunky_png-1.3.15/lib/chunky_png/ 0000755 0001750 0001750 00000000000 13766004353 016142 5 ustar daniel daniel chunky_png-1.3.15/lib/chunky_png/dimension.rb 0000644 0001750 0001750 00000011435 13766004353 020460 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# Creates a {ChunkyPNG::Dimension} instance using arguments that can be interpreted
# as width and height.
#
# @overload Dimension(width, height)
# @param [Integer] width The width-component of the dimension.
# @param [Integer] height The height-component of the dimension.
# @return [ChunkyPNG::Dimension] The instantiated dimension.
#
# @overload Dimension(string)
# @param [String] string A string from which a width and height value can be parsed, e.g.
# '10x20' or '[10, 20]'.
# @return [ChunkyPNG::Dimension] The instantiated dimension.
#
# @overload Dimension(ary)
# @param [Array] ary An array with the desired width as first element and the
# desired height as second element, e.g. [10, 20].
# @return [ChunkyPNG::Dimension] The instantiated dimension.
#
# @overload Dimension(hash)
# @param [Hash] hash An hash with a 'height' or :height key for the
# desired height and with a 'width' or :width key for the desired
# width.
# @return [ChunkyPNG::Dimension] The instantiated dimension.
#
# @return [ChunkyPNG::Dimension] The dimension created by this factory method.
# @raise [ArgumentError] If the argument(s) given where not understood as a dimension.
# @see ChunkyPNG::Dimension
def self.Dimension(*args)
case args.length
when 2 then ChunkyPNG::Dimension.new(*args)
when 1 then build_dimension_from_object(args.first)
else raise ArgumentError,
"Don't know how to construct a dimension from #{args.inspect}"
end
end
def self.build_dimension_from_object(source)
case source
when ChunkyPNG::Dimension
source
when ChunkyPNG::Point
ChunkyPNG::Dimension.new(source.x, source.y)
when Array
ChunkyPNG::Dimension.new(source[0], source[1])
when Hash
width = source[:width] || source["width"]
height = source[:height] || source["height"]
ChunkyPNG::Dimension.new(width, height)
when ChunkyPNG::Dimension::DIMENSION_REGEXP
ChunkyPNG::Dimension.new($1, $2)
else
if source.respond_to?(:width) && source.respond_to?(:height)
ChunkyPNG::Dimension.new(source.width, source.height)
else
raise ArgumentError, "Don't know how to construct a dimension from #{source.inspect}!"
end
end
end
private_class_method :build_dimension_from_object
# Class that represents the dimension of something, e.g. a {ChunkyPNG::Canvas}.
#
# This class contains some methods to simplify performing dimension related checks.
class Dimension
# @return [Regexp] The regexp to parse dimensions from a string.
# @private
DIMENSION_REGEXP = /^[\(\[\{]?(\d+)\s*[x,]?\s*(\d+)[\)\]\}]?$/
# @return [Integer] The width-component of this dimension.
attr_accessor :width
# @return [Integer] The height-component of this dimension.
attr_accessor :height
# Initializes a new dimension instance.
# @param [Integer] width The width-component of the new dimension.
# @param [Integer] height The height-component of the new dimension.
def initialize(width, height)
@width, @height = width.to_i, height.to_i
end
# Returns the area of this dimension.
# @return [Integer] The area in number of pixels.
def area
width * height
end
# Checks whether a point is within bounds of this dimension.
# @param [ChunkyPNG::Point, ...] point_like A point-like to bounds-check.
# @return [true, false] True iff the x and y coordinate fall in this dimension.
# @see ChunkyPNG.Point
def include?(*point_like)
point = ChunkyPNG::Point(*point_like)
point.x >= 0 && point.x < width && point.y >= 0 && point.y < height
end
# Checks whether 2 dimensions are identical.
# @param [ChunkyPNG::Dimension] other The dimension to compare with.
# @return [true, false] true iff width and height match.
def eql?(other)
return false unless other.respond_to?(:width) && other.respond_to?(:height)
other.width == width && other.height == height
end
alias == eql?
# Calculates a hash for the dimension object, based on width and height
# @return [Integer] A hashed value of the dimensions
def hash
[width, height].hash
end
# Compares the size of 2 dimensions.
# @param [ChunkyPNG::Dimension] other The dimension to compare with.
# @return [-1, 0, 1] -1 if the other dimension has a larger area, 1 of this
# dimension is larger, 0 if both are identical in size.
def <=>(other)
other.area <=> area
end
# Casts this dimension into an array.
# @return [Array] [width, height] for this dimension.
def to_a
[width, height]
end
alias to_ary to_a
end
end
chunky_png-1.3.15/lib/chunky_png/datastream.rb 0000644 0001750 0001750 00000015242 13766004353 020620 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# The Datastream class represents a PNG formatted datastream. It supports
# both reading from and writing to strings, streams and files.
#
# A PNG datastream begins with the PNG signature, and then contains multiple
# chunks, starting with a header (IHDR) chunk and finishing with an end
# (IEND) chunk.
#
# @see ChunkyPNG::Chunk
class Datastream
# The signature that each PNG file or stream should begin with.
SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10].pack("C8").force_encoding(::Encoding::BINARY).freeze
# The header chunk of this datastream.
# @return [ChunkyPNG::Chunk::Header]
attr_accessor :header_chunk
# All other chunks in this PNG file.
# @return [Array]
attr_accessor :other_chunks
# The chunk containing the image's palette.
# @return [ChunkyPNG::Chunk::Palette]
attr_accessor :palette_chunk
# The chunk containing the transparency information of the palette.
# @return [ChunkyPNG::Chunk::Transparency]
attr_accessor :transparency_chunk
# The chunk containing the physical dimensions of the PNG's pixels.
# @return [ChunkyPNG::Chunk::Physical]
attr_accessor :physical_chunk
# The chunks that together compose the images pixel data.
# @return [Array]
attr_accessor :data_chunks
# The empty chunk that signals the end of this datastream
# @return [ChunkyPNG::Chunk::Header]
attr_accessor :end_chunk
# Initializes a new Datastream instance.
def initialize
@other_chunks = []
@data_chunks = []
end
##############################################################################
# LOADING DATASTREAMS
##############################################################################
class << self
# Reads a PNG datastream from a string.
# @param [String] str The PNG encoded string to load from.
# @return [ChunkyPNG::Datastream] The loaded datastream instance.
def from_blob(str)
from_io(StringIO.new(str, "rb"))
end
alias from_string from_blob
# Reads a PNG datastream from a file.
# @param [String] filename The path of the file to load from.
# @return [ChunkyPNG::Datastream] The loaded datastream instance.
def from_file(filename)
ds = nil
File.open(filename, "rb") { |f| ds = from_io(f) }
ds
end
# Reads a PNG datastream from an input stream
# @param [IO] io The stream to read from.
# @return [ChunkyPNG::Datastream] The loaded datastream instance.
def from_io(io)
io.set_encoding(::Encoding::BINARY)
verify_signature!(io)
ds = new
while ds.end_chunk.nil?
chunk = ChunkyPNG::Chunk.read(io)
case chunk
when ChunkyPNG::Chunk::Header then ds.header_chunk = chunk
when ChunkyPNG::Chunk::Palette then ds.palette_chunk = chunk
when ChunkyPNG::Chunk::Transparency then ds.transparency_chunk = chunk
when ChunkyPNG::Chunk::ImageData then ds.data_chunks << chunk
when ChunkyPNG::Chunk::Physical then ds.physical_chunk = chunk
when ChunkyPNG::Chunk::End then ds.end_chunk = chunk
else ds.other_chunks << chunk
end
end
ds
end
# Verifies that the current stream is a PNG datastream by checking its signature.
#
# This method reads the PNG signature from the stream, setting the current position
# of the stream directly after the signature, where the IHDR chunk should begin.
#
# @param [IO] io The stream to read the PNG signature from.
# @raise [RuntimeError] An exception is raised if the PNG signature is not found at
# the beginning of the stream.
def verify_signature!(io)
signature = io.read(ChunkyPNG::Datastream::SIGNATURE.length)
unless signature == ChunkyPNG::Datastream::SIGNATURE
raise ChunkyPNG::SignatureMismatch, "PNG signature not found, found #{signature.inspect} instead of #{ChunkyPNG::Datastream::SIGNATURE.inspect}!"
end
end
end
##################################################################################
# CHUNKS
##################################################################################
# Enumerates the chunks in this datastream.
#
# This will iterate over the chunks using the order in which the chunks
# should appear in the PNG file.
#
# @yield [chunk] Yields the chunks in this datastream, one by one in the correct order.
# @yieldparam [ChunkyPNG::Chunk::Base] chunk A chunk in this datastream.
# @see ChunkyPNG::Datastream#chunks
def each_chunk
yield(header_chunk)
other_chunks.each { |chunk| yield(chunk) }
yield(palette_chunk) if palette_chunk
yield(transparency_chunk) if transparency_chunk
yield(physical_chunk) if physical_chunk
data_chunks.each { |chunk| yield(chunk) }
yield(end_chunk)
end
# Returns an enumerator instance for this datastream's chunks.
# @return [Enumerable::Enumerator] An enumerator for the :each_chunk method.
# @see ChunkyPNG::Datastream#each_chunk
def chunks
enum_for(:each_chunk)
end
# Returns all the textual metadata key/value pairs as hash.
# @return [Hash] A hash containing metadata fields and their values.
def metadata
metadata = {}
other_chunks.each do |chunk|
metadata[chunk.keyword] = chunk.value if chunk.respond_to?(:keyword) && chunk.respond_to?(:value)
end
metadata
end
# Returns the uncompressed image data, combined from all the IDAT chunks
# @return [String] The uncompressed image data for this datastream
def imagedata
ChunkyPNG::Chunk::ImageData.combine_chunks(data_chunks)
end
##################################################################################
# WRITING DATASTREAMS
##################################################################################
# Writes the datastream to the given output stream.
# @param [IO] io The output stream to write to.
def write(io)
io << SIGNATURE
each_chunk { |c| c.write(io) }
end
# Saves this datastream as a PNG file.
# @param [String] filename The filename to use.
def save(filename)
File.open(filename, "wb") { |f| write(f) }
end
# Encodes this datastream into a string.
# @return [String] The encoded PNG datastream.
def to_blob
str = StringIO.new
str.set_encoding("ASCII-8BIT")
write(str)
str.string
end
alias to_string to_blob
alias to_s to_blob
end
end
chunky_png-1.3.15/lib/chunky_png/color.rb 0000644 0001750 0001750 00000113321 13766004353 017606 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# Factory method to return a color value, based on the arguments given.
#
# @overload Color(r, g, b, a)
# @param (see ChunkyPNG::Color.rgba)
# @return [Integer] The rgba color value.
#
# @overload Color(r, g, b)
# @param (see ChunkyPNG::Color.rgb)
# @return [Integer] The rgb color value.
#
# @overload Color(hex_value, opacity = nil)
# @param (see ChunkyPNG::Color.from_hex)
# @return [Integer] The hex color value, with the opacity applied if one
# was given.
#
# @overload Color(color_name, opacity = nil)
# @param (see ChunkyPNG::Color.html_color)
# @return [Integer] The hex color value, with the opacity applied if one
# was given.
#
# @overload Color(color_value, opacity = nil)
# @param [Integer, :to_i] The color value.
# @return [Integer] The color value, with the opacity applied if one was
# given.
#
# @return [Integer] The determined color value as RGBA integer.
# @raise [ArgumentError] if the arguments weren't understood as a color.
# @see ChunkyPNG::Color
# @see ChunkyPNG::Color.parse
def self.Color(*args) # rubocop:disable Naming/MethodName # API backwards compatibility
case args.length
when 1 then ChunkyPNG::Color.parse(args.first)
when 2 then (ChunkyPNG::Color.parse(args.first) & 0xffffff00) | args[1].to_i
when 3 then ChunkyPNG::Color.rgb(*args)
when 4 then ChunkyPNG::Color.rgba(*args)
else raise ArgumentError, "Don't know how to create a color from #{args.inspect}!"
end
end
# rubocop:enable Naming/MethodName
# The Color module defines methods for handling colors. Within the ChunkyPNG
# library, the concepts of pixels and colors are both used, and they are
# both represented by a Integer.
#
# Pixels/colors are represented in RGBA components. Each of the four
# components is stored with a depth of 8 bits (maximum value = 255 =
# {ChunkyPNG::Color::MAX}). Together, these components are stored in a 4-byte
# Integer.
#
# A color will always be represented using these 4 components in memory.
# When the image is encoded, a more suitable representation can be used
# (e.g. rgb, grayscale, palette-based), for which several conversion methods
# are provided in this module.
module Color
extend self
# @return [Integer] The maximum value of each color component.
MAX = 0xff
# @private
# @return [Regexp] The regexp to parse 3-digit hex color values.
HEX3_COLOR_REGEXP = /\A(?:#|0x)?([0-9a-f]{3})\z/i
# @private
# @return [Regexp] The regexp to parse 6- and 8-digit hex color values.
HEX6_COLOR_REGEXP = /\A(?:#|0x)?([0-9a-f]{6})([0-9a-f]{2})?\z/i
# @private
# @return [Regexp] The regexp to parse named color values.
HTML_COLOR_REGEXP = /^([a-z][a-z_ ]+[a-z])(?:\ ?\@\ ?(1\.0|0\.\d+))?$/i
####################################################################
# CONSTRUCTING COLOR VALUES
####################################################################
# Parses a color value given a numeric or string argument.
#
# It supports color numbers, colors in hex notation and named HTML colors.
#
# @param [Integer, String] source The color value.
# @return [Integer] The color value, with the opacity applied if one was
# given.
def parse(source)
return source if source.is_a?(Integer)
case source.to_s
when /^\d+$/ then source.to_s.to_i
when HEX3_COLOR_REGEXP, HEX6_COLOR_REGEXP then from_hex(source.to_s)
when HTML_COLOR_REGEXP then html_color(source.to_s)
else raise ArgumentError, "Don't know how to create a color from #{source.inspect}!"
end
end
# Creates a new color using an r, g, b triple and an alpha value.
# @param [Integer] r The r-component (0-255)
# @param [Integer] g The g-component (0-255)
# @param [Integer] b The b-component (0-255)
# @param [Integer] a The opacity (0-255)
# @return [Integer] The newly constructed color value.
def rgba(r, g, b, a)
r << 24 | g << 16 | b << 8 | a
end
# Creates a new color using an r, g, b triple.
# @param [Integer] r The r-component (0-255)
# @param [Integer] g The g-component (0-255)
# @param [Integer] b The b-component (0-255)
# @return [Integer] The newly constructed color value.
def rgb(r, g, b)
r << 24 | g << 16 | b << 8 | 0xff
end
# Creates a new color using a grayscale teint.
# @param [Integer] teint The grayscale teint (0-255), will be used as r, g,
# and b value.
# @return [Integer] The newly constructed color value.
def grayscale(teint)
teint << 24 | teint << 16 | teint << 8 | 0xff
end
# Creates a new color using a grayscale teint and alpha value.
# @param [Integer] teint The grayscale teint (0-255), will be used as r, g,
# and b value.
# @param [Integer] a The opacity (0-255)
# @return [Integer] The newly constructed color value.
def grayscale_alpha(teint, a)
teint << 24 | teint << 16 | teint << 8 | a
end
####################################################################
# COLOR IMPORTING
####################################################################
# Creates a color by unpacking an rgb triple from a string.
#
# @param [String] stream The string to load the color from. It should be
# at least 3 + pos bytes long.
# @param [Integer] pos The position in the string to load the triple from.
# @return [Integer] The newly constructed color value.
def from_rgb_stream(stream, pos = 0)
rgb(*stream.unpack("@#{pos}C3"))
end
# Creates a color by unpacking an rgba triple from a string
#
# @param [String] stream The string to load the color from. It should be
# at least 4 + pos bytes long.
# @param [Integer] pos The position in the string to load the triple from.
# @return [Integer] The newly constructed color value.
def from_rgba_stream(stream, pos = 0)
rgba(*stream.unpack("@#{pos}C4"))
end
# Creates a color by converting it from a string in hex notation.
#
# It supports colors with (#rrggbbaa) or without (#rrggbb) alpha channel
# as well as the 3-digit short format (#rgb) for those without.
# Color strings may include the prefix "0x" or "#".
#
# @param [String] hex_value The color in hex notation.
# @param [Integer] opacity The opacity value for the color. Overrides any
# opacity value given in the hex value if given.
# @return [Integer] The color value.
# @raise [ArgumentError] if the value given is not a hex color notation.
def from_hex(hex_value, opacity = nil)
base_color = case hex_value
when HEX3_COLOR_REGEXP
$1.gsub(/([0-9a-f])/i, '\1\1').hex << 8
when HEX6_COLOR_REGEXP
$1.hex << 8
else
raise ArgumentError, "Not a valid hex color notation: #{hex_value.inspect}!"
end
opacity ||= $2 ? $2.hex : 0xff
base_color | opacity
end
# Creates a new color from an HSV triple.
#
# Create a new color using an HSV (sometimes also called HSB) triple. The
# words `value` and `brightness` are used interchangeably and synonymously
# in descriptions of this colorspace. This implementation follows the modern
# convention of 0 degrees hue indicating red.
#
# @param [Fixnum] hue The hue component (0-360)
# @param [Fixnum] saturation The saturation component (0-1)
# @param [Fixnum] value The value (brightness) component (0-1)
# @param [Fixnum] alpha Defaults to opaque (255).
# @return [Integer] The newly constructed color value.
# @raise [ArgumentError] if the hsv triple is invalid.
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
def from_hsv(hue, saturation, value, alpha = 255)
raise ArgumentError, "Hue must be between 0 and 360" unless (0..360).cover?(hue)
raise ArgumentError, "Saturation must be between 0 and 1" unless (0..1).cover?(saturation)
raise ArgumentError, "Value/brightness must be between 0 and 1" unless (0..1).cover?(value)
chroma = value * saturation
rgb = cylindrical_to_cubic(hue, saturation, value, chroma)
rgb.map! { |component| ((component + value - chroma) * 255).to_i }
rgb << alpha
rgba(*rgb)
end
alias from_hsb from_hsv
# Creates a new color from an HSL triple.
#
# This implementation follows the modern convention of 0 degrees hue
# indicating red.
#
# @param [Fixnum] hue The hue component (0-360)
# @param [Fixnum] saturation The saturation component (0-1)
# @param [Fixnum] lightness The lightness component (0-1)
# @param [Fixnum] alpha Defaults to opaque (255).
# @return [Integer] The newly constructed color value.
# @raise [ArgumentError] if the hsl triple is invalid.
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
def from_hsl(hue, saturation, lightness, alpha = 255)
raise ArgumentError, "Hue #{hue} was not between 0 and 360" unless (0..360).cover?(hue)
raise ArgumentError, "Saturation #{saturation} was not between 0 and 1" unless (0..1).cover?(saturation)
raise ArgumentError, "Lightness #{lightness} was not between 0 and 1" unless (0..1).cover?(lightness)
chroma = (1 - (2 * lightness - 1).abs) * saturation
rgb = cylindrical_to_cubic(hue, saturation, lightness, chroma)
rgb.map! { |component| ((component + lightness - 0.5 * chroma) * 255).to_i }
rgb << alpha
rgba(*rgb)
end
# Convert one HSL or HSV triple and associated chroma to a scaled rgb triple
#
# This method encapsulates the shared mathematical operations needed to
# convert coordinates from a cylindrical colorspace such as HSL or HSV into
# coordinates of the RGB colorspace.
#
# Even though chroma values are derived from the other three coordinates,
# the formula for calculating chroma differs for each colorspace. Since it
# is calculated differently for each colorspace, it must be passed in as
# a parameter.
#
# @param [Fixnum] hue The hue-component (0-360)
# @param [Fixnum] saturation The saturation-component (0-1)
# @param [Fixnum] y_component The y_component can represent either lightness
# or brightness/value (0-1) depending on which scheme (HSV/HSL) is being used.
# @param [Fixnum] chroma The associated chroma value.
# @return [Array] A scaled r,g,b triple. Scheme-dependent
# adjustments are still needed to reach the true r,g,b values.
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
# @private
def cylindrical_to_cubic(hue, saturation, y_component, chroma)
hue_prime = hue.fdiv(60)
x = chroma * (1 - (hue_prime % 2 - 1).abs)
case hue_prime
when (0...1) then [chroma, x, 0]
when (1...2) then [x, chroma, 0]
when (2...3) then [0, chroma, x]
when (3...4) then [0, x, chroma]
when (4...5) then [x, 0, chroma]
when (5..6) then [chroma, 0, x]
end
end
private :cylindrical_to_cubic
####################################################################
# PROPERTIES
####################################################################
# Returns the red-component from the color value.
#
# @param [Integer] value The color value.
# @return [Integer] A value between 0 and MAX.
def r(value)
(value & 0xff000000) >> 24
end
# Returns the green-component from the color value.
#
# @param [Integer] value The color value.
# @return [Integer] A value between 0 and MAX.
def g(value)
(value & 0x00ff0000) >> 16
end
# Returns the blue-component from the color value.
#
# @param [Integer] value The color value.
# @return [Integer] A value between 0 and MAX.
def b(value)
(value & 0x0000ff00) >> 8
end
# Returns the alpha channel value for the color value.
#
# @param [Integer] value The color value.
# @return [Integer] A value between 0 and MAX.
def a(value)
value & 0x000000ff
end
# Returns true if this color is fully opaque.
#
# @param [Integer] value The color to test.
# @return [true, false] True if the alpha channel equals MAX.
def opaque?(value)
a(value) == 0x000000ff
end
# Returns the opaque value of this color by removing the alpha channel.
# @param [Integer] value The color to transform.
# @return [Integer] The opaque color
def opaque!(value)
value | 0x000000ff
end
# Returns true if this color is fully transparent.
#
# @param [Integer] value The color to test.
# @return [true, false] True if the r, g and b component are equal.
def grayscale?(value)
r(value) == b(value) && b(value) == g(value)
end
# Returns true if this color is fully transparent.
#
# @param [Integer] value The color to test.
# @return [true, false] True if the alpha channel equals 0.
def fully_transparent?(value)
a(value) == 0x00000000
end
####################################################################
# ALPHA COMPOSITION
####################################################################
# Multiplies two fractions using integer math, where the fractions are
# stored using an integer between 0 and 255. This method is used as a
# helper method for compositing colors using integer math.
#
# This is a quicker implementation of ((a * b) / 255.0).round.
#
# @param [Integer] a The first fraction.
# @param [Integer] b The second fraction.
# @return [Integer] The result of the multiplication.
def int8_mult(a, b)
t = a * b + 0x80
((t >> 8) + t) >> 8
end
# Composes two colors with an alpha channel using integer math.
#
# This version is faster than the version based on floating point math, so
# this compositing function is used by default.
#
# @param [Integer] fg The foreground color.
# @param [Integer] bg The background color.
# @return [Integer] The composited color.
# @see ChunkyPNG::Color#compose_precise
def compose_quick(fg, bg)
return fg if opaque?(fg) || fully_transparent?(bg)
return bg if fully_transparent?(fg)
a_com = int8_mult(0xff - a(fg), a(bg))
new_r = int8_mult(a(fg), r(fg)) + int8_mult(a_com, r(bg))
new_g = int8_mult(a(fg), g(fg)) + int8_mult(a_com, g(bg))
new_b = int8_mult(a(fg), b(fg)) + int8_mult(a_com, b(bg))
new_a = a(fg) + a_com
rgba(new_r, new_g, new_b, new_a)
end
# Composes two colors with an alpha channel using floating point math.
#
# This method uses more precise floating point math, but this precision is
# lost when the result is converted back to an integer. Because it is
# slower than the version based on integer math, that version is preferred.
#
# @param [Integer] fg The foreground color.
# @param [Integer] bg The background color.
# @return [Integer] The composited color.
# @see ChunkyPNG::Color#compose_quick
def compose_precise(fg, bg)
return fg if opaque?(fg) || fully_transparent?(bg)
return bg if fully_transparent?(fg)
fg_a = a(fg).to_f / MAX
bg_a = a(bg).to_f / MAX
a_com = (1.0 - fg_a) * bg_a
new_r = (fg_a * r(fg) + a_com * r(bg)).round
new_g = (fg_a * g(fg) + a_com * g(bg)).round
new_b = (fg_a * b(fg) + a_com * b(bg)).round
new_a = ((fg_a + a_com) * MAX).round
rgba(new_r, new_g, new_b, new_a)
end
alias compose compose_quick
# Blends the foreground and background color by taking the average of
# the components.
#
# @param [Integer] fg The foreground color.
# @param [Integer] bg The foreground color.
# @return [Integer] The blended color.
def blend(fg, bg)
(fg + bg) >> 1
end
# Interpolates the foreground and background colors by the given alpha
# value. This also blends the alpha channels themselves.
#
# A blending factor of 255 will give entirely the foreground,
# while a blending factor of 0 will give the background.
#
# @param [Integer] fg The foreground color.
# @param [Integer] bg The background color.
# @param [Integer] alpha The blending factor (fixed 8bit)
# @return [Integer] The interpolated color.
def interpolate_quick(fg, bg, alpha)
return fg if alpha >= 255
return bg if alpha <= 0
alpha_com = 255 - alpha
new_r = int8_mult(alpha, r(fg)) + int8_mult(alpha_com, r(bg))
new_g = int8_mult(alpha, g(fg)) + int8_mult(alpha_com, g(bg))
new_b = int8_mult(alpha, b(fg)) + int8_mult(alpha_com, b(bg))
new_a = int8_mult(alpha, a(fg)) + int8_mult(alpha_com, a(bg))
rgba(new_r, new_g, new_b, new_a)
end
# Calculates the grayscale teint of an RGB color.
#
# @param [Integer] color The color to convert.
# @return [Integer] The grayscale teint of the input color, 0-255.
def grayscale_teint(color)
(r(color) * 0.3 + g(color) * 0.59 + b(color) * 0.11).round
end
# Converts a color to a fiting grayscale value. It will conserve the alpha
# channel.
#
# This method will return a full color value, with the R, G, and B value
# set to the grayscale teint calcuated from the input color's R, G and B
# values.
#
# @param [Integer] color The color to convert.
# @return [Integer] The input color, converted to the best fitting
# grayscale.
# @see #grayscale_teint
def to_grayscale(color)
grayscale_alpha(grayscale_teint(color), a(color))
end
# Lowers the intensity of a color, by lowering its alpha by a given factor.
# @param [Integer] color The color to adjust.
# @param [Integer] factor Fade factor as an integer between 0 and 255.
# @return [Integer] The faded color.
def fade(color, factor)
new_alpha = int8_mult(a(color), factor)
(color & 0xffffff00) | new_alpha
end
# Decomposes a color, given a color, a mask color and a background color.
# The returned color will be a variant of the mask color, with the alpha
# channel set to the best fitting value. This basically is the reverse
# operation if alpha composition.
#
# If the color cannot be decomposed, this method will return the fully
# transparent variant of the mask color.
#
# @param [Integer] color The color that was the result of compositing.
# @param [Integer] mask The opaque variant of the color that was being
# composed
# @param [Integer] bg The background color on which the color was composed.
# @param [Integer] tolerance The decomposition tolerance level, a value
# between 0 and 255.
# @return [Integer] The decomposed color, a variant of the masked color
# with the alpha channel set to an appropriate value.
def decompose_color(color, mask, bg, tolerance = 1)
if alpha_decomposable?(color, mask, bg, tolerance)
mask & 0xffffff00 | decompose_alpha(color, mask, bg)
else
mask & 0xffffff00
end
end
# Checks whether an alpha channel value can successfully be composed
# given the resulting color, the mask color and a background color,
# all of which should be opaque.
#
# @param [Integer] color The color that was the result of compositing.
# @param [Integer] mask The opaque variant of the color that was being
# composed
# @param [Integer] bg The background color on which the color was composed.
# @param [Integer] tolerance The decomposition tolerance level, a value
# between 0 and 255.
# @return [Boolean] True if the alpha component can be decomposed
# successfully.
# @see #decompose_alpha
def alpha_decomposable?(color, mask, bg, tolerance = 1)
components = decompose_alpha_components(color, mask, bg)
sum = components.inject(0) { |a, b| a + b }
max = components.max * 3
components.max <= 255 && components.min >= 0 && (sum + tolerance * 3) >= max
end
# Decomposes the alpha channel value given the resulting color, the mask
# color and a background color, all of which should be opaque.
#
# Make sure to call {#alpha_decomposable?} first to see if the alpha
# channel value can successfully decomposed with a given tolerance,
# otherwise the return value of this method is undefined.
#
# @param [Integer] color The color that was the result of compositing.
# @param [Integer] mask The opaque variant of the color that was being
# composed
# @param [Integer] bg The background color on which the color was composed.
# @return [Integer] The best fitting alpha channel, a value between 0 and
# 255.
# @see #alpha_decomposable?
def decompose_alpha(color, mask, bg)
components = decompose_alpha_components(color, mask, bg)
(components.inject(0) { |a, b| a + b } / 3.0).round
end
# Decomposes an alpha channel for either the r, g or b color channel.
# @param [:r, :g, :b] channel The channel to decompose the alpha channel
# from.
# @param [Integer] color The color that was the result of compositing.
# @param [Integer] mask The opaque variant of the color that was being
# composed
# @param [Integer] bg The background color on which the color was composed.
# @return [Integer] The decomposed alpha value for the channel.
def decompose_alpha_component(channel, color, mask, bg)
cc, mc, bc = send(channel, color), send(channel, mask), send(channel, bg)
return 0x00 if bc == cc
return 0xff if bc == mc
return 0xff if cc == mc
(((bc - cc).to_f / (bc - mc).to_f) * MAX).round
end
# Decomposes the alpha channels for the r, g and b color channel.
# @param [Integer] color The color that was the result of compositing.
# @param [Integer] mask The opaque variant of the color that was being
# composed
# @param [Integer] bg The background color on which the color was composed.
# @return [Array] The decomposed alpha values for the r, g and b
# channels.
def decompose_alpha_components(color, mask, bg)
[
decompose_alpha_component(:r, color, mask, bg),
decompose_alpha_component(:g, color, mask, bg),
decompose_alpha_component(:b, color, mask, bg),
]
end
####################################################################
# CONVERSIONS
####################################################################
# Returns a string representing this color using hex notation (i.e.
# #rrggbbaa).
#
# @param [Integer] color The color to convert.
# @param [Boolean] include_alpha
# @return [String] The color in hex notation, starting with a pound sign.
def to_hex(color, include_alpha = true)
include_alpha ? ("#%08x" % color) : ("#%06x" % [color >> 8])
end
# Returns an array with the separate HSV components of a color.
#
# Because ChunkyPNG internally handles colors as Integers for performance
# reasons, some rounding occurs when importing or exporting HSV colors
# whose coordinates are float-based. Because of this rounding, #to_hsv and
# #from_hsv may not be perfect inverses.
#
# This implementation follows the modern convention of 0 degrees hue
# indicating red.
#
# @param [Integer] color The ChunkyPNG color to convert.
# @param [Boolean] include_alpha Flag indicates whether a fourth element
# representing alpha channel should be included in the returned array.
# @return [Array[0]] The hue of the color (0-360)
# @return [Array[1]] The saturation of the color (0-1)
# @return [Array[2]] The value of the color (0-1)
# @return [Array[3]] Optional fourth element for alpha, included if
# include_alpha=true (0-255)
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
def to_hsv(color, include_alpha = false)
hue, chroma, max, _ = hue_and_chroma(color)
value = max
saturation = chroma.zero? ? 0.0 : chroma.fdiv(value)
include_alpha ? [hue, saturation, value, a(color)] :
[hue, saturation, value]
end
alias to_hsb to_hsv
# Returns an array with the separate HSL components of a color.
#
# Because ChunkyPNG internally handles colors as Integers for performance
# reasons, some rounding occurs when importing or exporting HSL colors
# whose coordinates are float-based. Because of this rounding, #to_hsl and
# #from_hsl may not be perfect inverses.
#
# This implementation follows the modern convention of 0 degrees hue indicating red.
#
# @param [Integer] color The ChunkyPNG color to convert.
# @param [Boolean] include_alpha Flag indicates whether a fourth element
# representing alpha channel should be included in the returned array.
# @return [Array[0]] The hue of the color (0-360)
# @return [Array[1]] The saturation of the color (0-1)
# @return [Array[2]] The lightness of the color (0-1)
# @return [Array[3]] Optional fourth element for alpha, included if
# include_alpha=true (0-255)
# @see https://en.wikipedia.org/wiki/HSL_and_HSV
def to_hsl(color, include_alpha = false)
hue, chroma, max, min = hue_and_chroma(color)
lightness = 0.5 * (max + min)
saturation = chroma.zero? ? 0.0 : chroma.fdiv(1 - (2 * lightness - 1).abs)
include_alpha ? [hue, saturation, lightness, a(color)] :
[hue, saturation, lightness]
end
# This method encapsulates the logic needed to extract hue and chroma from
# a ChunkPNG color. This logic is shared by the cylindrical HSV/HSB and HSL
# color space models.
#
# @param [Integer] color A ChunkyPNG color.
# @return [Fixnum] hue The hue of the color (0-360)
# @return [Fixnum] chroma The chroma of the color (0-1)
# @return [Fixnum] max The magnitude of the largest scaled rgb component (0-1)
# @return [Fixnum] min The magnitude of the smallest scaled rgb component (0-1)
# @private
def hue_and_chroma(color)
scaled_rgb = to_truecolor_bytes(color)
scaled_rgb.map! { |component| component.fdiv(255) }
min, max = scaled_rgb.minmax
chroma = max - min
r, g, b = scaled_rgb
hue_prime = chroma.zero? ? 0 : case max
when r then (g - b).fdiv(chroma)
when g then (b - r).fdiv(chroma) + 2
when b then (r - g).fdiv(chroma) + 4
else 0
end
hue = 60 * hue_prime
[hue.round, chroma, max, min]
end
private :hue_and_chroma
# Returns an array with the separate RGBA values for this color.
#
# @param [Integer] color The color to convert.
# @return [Array] An array with 4 Integer elements.
def to_truecolor_alpha_bytes(color)
[r(color), g(color), b(color), a(color)]
end
# Returns an array with the separate RGB values for this color. The alpha
# channel will be discarded.
#
# @param [Integer] color The color to convert.
# @return [Array] An array with 3 Integer elements.
def to_truecolor_bytes(color)
[r(color), g(color), b(color)]
end
# Returns an array with the grayscale teint value for this color.
#
# This method expects the r, g, and b value to be equal, and the alpha
# channel will be discarded.
#
# @param [Integer] color The grayscale color to convert.
# @return [Array] An array with 1 Integer element.
def to_grayscale_bytes(color)
[b(color)] # assumption r == g == b
end
# Returns an array with the grayscale teint and alpha channel values for
# this color.
#
# This method expects the color to be grayscale, i.e. r, g, and b value to
# be equal and uses only the B channel. If you need to convert a color to
# grayscale first, see {#to_grayscale}.
#
# @param [Integer] color The grayscale color to convert.
# @return [Array] An array with 2 Integer elements.
# @see #to_grayscale
def to_grayscale_alpha_bytes(color)
[b(color), a(color)] # assumption r == g == b
end
####################################################################
# COMPARISON
####################################################################
# Compute the Euclidean distance between 2 colors in RGBA
#
# This method simply takes the Euclidean distance between the RGBA channels
# of 2 colors, which gives us a measure of how different the two colors
# are.
#
# Although it would be more perceptually accurate to calculate a proper
# Delta E in Lab colorspace, this method should serve many use-cases while
# avoiding the overhead of converting RGBA to Lab.
#
# @param pixel_after [Integer]
# @param pixel_before [Integer]
# @return [Float]
def euclidean_distance_rgba(pixel_after, pixel_before)
return 0.0 if pixel_after == pixel_before
Math.sqrt(
(r(pixel_after) - r(pixel_before))**2 +
(g(pixel_after) - g(pixel_before))**2 +
(b(pixel_after) - b(pixel_before))**2 +
(a(pixel_after) - a(pixel_before))**2
)
end
# Could be simplified as MAX * 2, but this format mirrors the math in
# {#euclidean_distance_rgba}
# @return [Float] The maximum Euclidean distance of two RGBA colors.
MAX_EUCLIDEAN_DISTANCE_RGBA = Math.sqrt(MAX**2 * 4)
####################################################################
# COLOR CONSTANTS
####################################################################
# @return [Hash] All the predefined color names in HTML.
PREDEFINED_COLORS = {
aliceblue: 0xf0f8ff00,
antiquewhite: 0xfaebd700,
aqua: 0x00ffff00,
aquamarine: 0x7fffd400,
azure: 0xf0ffff00,
beige: 0xf5f5dc00,
bisque: 0xffe4c400,
black: 0x00000000,
blanchedalmond: 0xffebcd00,
blue: 0x0000ff00,
blueviolet: 0x8a2be200,
brown: 0xa52a2a00,
burlywood: 0xdeb88700,
cadetblue: 0x5f9ea000,
chartreuse: 0x7fff0000,
chocolate: 0xd2691e00,
coral: 0xff7f5000,
cornflowerblue: 0x6495ed00,
cornsilk: 0xfff8dc00,
crimson: 0xdc143c00,
cyan: 0x00ffff00,
darkblue: 0x00008b00,
darkcyan: 0x008b8b00,
darkgoldenrod: 0xb8860b00,
darkgray: 0xa9a9a900,
darkgrey: 0xa9a9a900,
darkgreen: 0x00640000,
darkkhaki: 0xbdb76b00,
darkmagenta: 0x8b008b00,
darkolivegreen: 0x556b2f00,
darkorange: 0xff8c0000,
darkorchid: 0x9932cc00,
darkred: 0x8b000000,
darksalmon: 0xe9967a00,
darkseagreen: 0x8fbc8f00,
darkslateblue: 0x483d8b00,
darkslategray: 0x2f4f4f00,
darkslategrey: 0x2f4f4f00,
darkturquoise: 0x00ced100,
darkviolet: 0x9400d300,
deeppink: 0xff149300,
deepskyblue: 0x00bfff00,
dimgray: 0x69696900,
dimgrey: 0x69696900,
dodgerblue: 0x1e90ff00,
firebrick: 0xb2222200,
floralwhite: 0xfffaf000,
forestgreen: 0x228b2200,
fuchsia: 0xff00ff00,
gainsboro: 0xdcdcdc00,
ghostwhite: 0xf8f8ff00,
gold: 0xffd70000,
goldenrod: 0xdaa52000,
gray: 0x80808000,
grey: 0x80808000,
green: 0x00800000,
greenyellow: 0xadff2f00,
honeydew: 0xf0fff000,
hotpink: 0xff69b400,
indianred: 0xcd5c5c00,
indigo: 0x4b008200,
ivory: 0xfffff000,
khaki: 0xf0e68c00,
lavender: 0xe6e6fa00,
lavenderblush: 0xfff0f500,
lawngreen: 0x7cfc0000,
lemonchiffon: 0xfffacd00,
lightblue: 0xadd8e600,
lightcoral: 0xf0808000,
lightcyan: 0xe0ffff00,
lightgoldenrodyellow: 0xfafad200,
lightgray: 0xd3d3d300,
lightgrey: 0xd3d3d300,
lightgreen: 0x90ee9000,
lightpink: 0xffb6c100,
lightsalmon: 0xffa07a00,
lightseagreen: 0x20b2aa00,
lightskyblue: 0x87cefa00,
lightslategray: 0x77889900,
lightslategrey: 0x77889900,
lightsteelblue: 0xb0c4de00,
lightyellow: 0xffffe000,
lime: 0x00ff0000,
limegreen: 0x32cd3200,
linen: 0xfaf0e600,
magenta: 0xff00ff00,
maroon: 0x80000000,
mediumaquamarine: 0x66cdaa00,
mediumblue: 0x0000cd00,
mediumorchid: 0xba55d300,
mediumpurple: 0x9370d800,
mediumseagreen: 0x3cb37100,
mediumslateblue: 0x7b68ee00,
mediumspringgreen: 0x00fa9a00,
mediumturquoise: 0x48d1cc00,
mediumvioletred: 0xc7158500,
midnightblue: 0x19197000,
mintcream: 0xf5fffa00,
mistyrose: 0xffe4e100,
moccasin: 0xffe4b500,
navajowhite: 0xffdead00,
navy: 0x00008000,
oldlace: 0xfdf5e600,
olive: 0x80800000,
olivedrab: 0x6b8e2300,
orange: 0xffa50000,
orangered: 0xff450000,
orchid: 0xda70d600,
palegoldenrod: 0xeee8aa00,
palegreen: 0x98fb9800,
paleturquoise: 0xafeeee00,
palevioletred: 0xd8709300,
papayawhip: 0xffefd500,
peachpuff: 0xffdab900,
peru: 0xcd853f00,
pink: 0xffc0cb00,
plum: 0xdda0dd00,
powderblue: 0xb0e0e600,
purple: 0x80008000,
red: 0xff000000,
rosybrown: 0xbc8f8f00,
royalblue: 0x4169e100,
saddlebrown: 0x8b451300,
salmon: 0xfa807200,
sandybrown: 0xf4a46000,
seagreen: 0x2e8b5700,
seashell: 0xfff5ee00,
sienna: 0xa0522d00,
silver: 0xc0c0c000,
skyblue: 0x87ceeb00,
slateblue: 0x6a5acd00,
slategray: 0x70809000,
slategrey: 0x70809000,
snow: 0xfffafa00,
springgreen: 0x00ff7f00,
steelblue: 0x4682b400,
tan: 0xd2b48c00,
teal: 0x00808000,
thistle: 0xd8bfd800,
tomato: 0xff634700,
turquoise: 0x40e0d000,
violet: 0xee82ee00,
wheat: 0xf5deb300,
white: 0xffffff00,
whitesmoke: 0xf5f5f500,
yellow: 0xffff0000,
yellowgreen: 0x9acd3200,
}
# Gets a color value based on a HTML color name.
#
# The color name is flexible. E.g. 'yellowgreen', 'Yellow
# green', 'YellowGreen', 'YELLOW_GREEN' and
# :yellow_green will all return the same color value.
#
# You can include a opacity level in the color name (e.g. 'red @
# 0.5') or give an explicit opacity value as second argument. If no
# opacity value is given, the color will be fully opaque.
#
# @param [Symbol, String] color_name The color name. It may include an
# opacity specifier like @ 0.8 to set the color's opacity.
# @param [Integer] opacity The opacity value for the color between 0 and
# 255. Overrides any opacity value given in the color name.
# @return [Integer] The color value.
# @raise [ChunkyPNG::Exception] If the color name was not recognized.
def html_color(color_name, opacity = nil)
if color_name.to_s =~ HTML_COLOR_REGEXP
opacity ||= $2 ? ($2.to_f * 255.0).round : 0xff
base_color_name = $1.gsub(/[^a-z]+/i, "").downcase.to_sym
return PREDEFINED_COLORS[base_color_name] | opacity if PREDEFINED_COLORS.key?(base_color_name)
end
raise ArgumentError, "Unknown color name #{color_name}!"
end
# @return [Integer] Black pixel/color
BLACK = rgb(0, 0, 0)
# @return [Integer] White pixel/color
WHITE = rgb(255, 255, 255)
# @return [Integer] Fully transparent pixel/color
TRANSPARENT = rgba(0, 0, 0, 0)
####################################################################
# STATIC UTILITY METHODS
####################################################################
# Returns the number of sample values per pixel.
# @param [Integer] color_mode The color mode being used.
# @return [Integer] The number of sample values per pixel.
def samples_per_pixel(color_mode)
case color_mode
when ChunkyPNG::COLOR_INDEXED then 1
when ChunkyPNG::COLOR_TRUECOLOR then 3
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then 4
when ChunkyPNG::COLOR_GRAYSCALE then 1
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then 2
else raise ChunkyPNG::NotSupported, "Don't know the number of samples for this colormode: #{color_mode}!"
end
end
# Returns the size in bytes of a pixel when it is stored using a given
# color mode.
#
# @param [Integer] color_mode The color mode in which the pixels are
# stored.
# @return [Integer] The number of bytes used per pixel in a datastream.
def pixel_bytesize(color_mode, depth = 8)
return 1 if depth < 8
(pixel_bitsize(color_mode, depth) + 7) >> 3
end
# Returns the size in bits of a pixel when it is stored using a given color
# mode.
#
# @param [Integer] color_mode The color mode in which the pixels are
# stored.
# @param [Integer] depth The color depth of the pixels.
# @return [Integer] The number of bytes used per pixel in a datastream.
def pixel_bitsize(color_mode, depth = 8)
samples_per_pixel(color_mode) * depth
end
# Returns the number of bytes used per scanline.
# @param [Integer] color_mode The color mode in which the pixels are
# stored.
# @param [Integer] depth The color depth of the pixels.
# @param [Integer] width The number of pixels per scanline.
# @return [Integer] The number of bytes used per scanline in a datastream.
def scanline_bytesize(color_mode, depth, width)
((pixel_bitsize(color_mode, depth) * width) + 7) >> 3
end
# Returns the number of bytes used for an image pass
# @param [Integer] color_mode The color mode in which the pixels are
# stored.
# @param [Integer] depth The color depth of the pixels.
# @param [Integer] width The width of the image pass.
# @param [Integer] height The height of the image pass.
# @return [Integer] The number of bytes used per scanline in a datastream.
def pass_bytesize(color_mode, depth, width, height)
return 0 if width == 0 || height == 0
(scanline_bytesize(color_mode, depth, width) + 1) * height
end
end
end
chunky_png-1.3.15/lib/chunky_png/palette.rb 0000644 0001750 0001750 00000017507 13766004353 020137 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# A palette describes the set of colors that is being used for an image.
#
# A PNG image can contain an explicit palette which defines the colors of
# that image, but can also use an implicit palette, e.g. all truecolor colors
# or all grayscale colors.
#
# This palette supports decoding colors from a palette if an explicit palette
# is provided in a PNG datastream, and it supports encoding colors to an
# explicit palette (stores as PLTE & tRNS chunks in a PNG file).
#
# @see ChunkyPNG::Color
class Palette < Set
# Builds a new palette given a set (Enumerable instance) of colors.
#
# @param enum [Enumerable] The set of colors to include in this
# palette.This Enumerable can contain duplicates.
# @param decoding_map [Array] An array of colors in the exact order at
# which they appeared in the palette chunk, so that this array can be
# used for decoding.
def initialize(enum, decoding_map = nil)
super(enum.sort.freeze)
@decoding_map = decoding_map if decoding_map
@encoding_map = {}
freeze
end
# Builds a palette instance from a PLTE chunk and optionally a tRNS chunk
# from a PNG datastream.
#
# This method will cerate a palette that is suitable for decoding an image.
#
# @param palette_chunk [ChunkyPNG::Chunk::Palette] The palette chunk to
# load from
# @param transparency_chunk [ChunkyPNG::Chunk::Transparency, nil] The
# optional transparency chunk.
# @return [ChunkyPNG::Palette] The loaded palette instance.
# @see ChunkyPNG::Palette#can_decode?
def self.from_chunks(palette_chunk, transparency_chunk = nil)
return nil if palette_chunk.nil?
decoding_map = []
index = 0
palatte_bytes = palette_chunk.content.unpack("C*")
alpha_channel = transparency_chunk ? transparency_chunk.content.unpack("C*") : []
index = 0
palatte_bytes.each_slice(3) do |bytes|
bytes << alpha_channel.fetch(index, ChunkyPNG::Color::MAX)
decoding_map << ChunkyPNG::Color.rgba(*bytes)
index += 1
end
new(decoding_map, decoding_map)
end
# Builds a palette instance from a given canvas.
# @param canvas [ChunkyPNG::Canvas] The canvas to create a palette for.
# @return [ChunkyPNG::Palette] The palette instance.
def self.from_canvas(canvas)
# Although we don't need to call .uniq.sort before initializing, because
# Palette subclasses SortedSet, we get significantly better performance
# by doing so.
new(canvas.pixels.uniq.sort)
end
# Builds a palette instance from a given set of pixels.
# @param pixels [Enumerable] An enumeration of pixels to create a
# palette for
# @return [ChunkyPNG::Palette] The palette instance.
def self.from_pixels(pixels)
new(pixels)
end
# Checks whether the size of this palette is suitable for indexed storage.
# @return [true, false] True if the number of colors in this palette is at
# most 256.
def indexable?
size <= 256
end
# Check whether this palette only contains opaque colors.
# @return [true, false] True if all colors in this palette are opaque.
# @see ChunkyPNG::Color#opaque?
def opaque?
all? { |color| Color.opaque?(color) }
end
# Check whether this palette only contains grayscale colors.
# @return [true, false] True if all colors in this palette are grayscale
# teints.
# @see ChunkyPNG::Color#grayscale??
def grayscale?
all? { |color| Color.grayscale?(color) }
end
# Check whether this palette only contains bacl and white.
# @return [true, false] True if all colors in this palette are grayscale
# teints.
# @see ChunkyPNG::Color#grayscale??
def black_and_white?
entries == [ChunkyPNG::Color::BLACK, ChunkyPNG::Color::WHITE]
end
# Returns a palette with all the opaque variants of the colors in this
# palette.
# @return [ChunkyPNG::Palette] A new Palette instance with only opaque
# colors.
# @see ChunkyPNG::Color#opaque!
def opaque_palette
self.class.new(map { |c| ChunkyPNG::Color.opaque!(c) })
end
# Checks whether this palette is suitable for decoding an image from a
# datastream.
#
# This requires that the positions of the colors in the original palette
# chunk is known, which is stored as an array in the +@decoding_map+
# instance variable.
#
# @return [true, false] True if a decoding map was built when this palette
# was loaded.
def can_decode?
!@decoding_map.nil?
end
# Checks whether this palette is suitable for encoding an image from to
# datastream.
#
# This requires that the position of the color in the future palette chunk
# is known, which is stored as a hash in the +@encoding_map+ instance
# variable.
#
# @return [true, false] True if a encoding map was built when this palette
# was loaded.
def can_encode?
!@encoding_map.empty?
end
# Returns a color, given the position in the original palette chunk.
# @param index [Integer] The 0-based position of the color in the palette.
# @return [ChunkyPNG::Color] The color that is stored in the palette under
# the given index
# @see ChunkyPNG::Palette#can_decode?
def [](index)
@decoding_map[index]
end
# Returns the position of a color in the palette
# @param color [ChunkyPNG::Color] The color for which to look up the index.
# @return [Integer] The 0-based position of the color in the palette.
# @see ChunkyPNG::Palette#can_encode?
def index(color)
color.nil? ? 0 : @encoding_map[color]
end
# Creates a tRNS chunk that corresponds with this palette to store the
# alpha channel of all colors.
#
# Note that this chunk can be left out of every color in the palette is
# opaque, and the image is encoded using indexed colors.
#
# @return [ChunkyPNG::Chunk::Transparency] The tRNS chunk.
def to_trns_chunk
ChunkyPNG::Chunk::Transparency.new("tRNS", map { |c| ChunkyPNG::Color.a(c) }.pack("C*"))
end
# Creates a PLTE chunk that corresponds with this palette to store the r,
# g, and b channels of all colors.
#
# @note A PLTE chunk should only be included if the image is encoded using
# index colors. After this chunk has been built, the palette becomes
# suitable for encoding an image.
#
# @return [ChunkyPNG::Chunk::Palette] The PLTE chunk.
# @see ChunkyPNG::Palette#can_encode?
def to_plte_chunk
@encoding_map.clear
colors = []
each_with_index do |color, index|
@encoding_map[color] = index
colors += ChunkyPNG::Color.to_truecolor_bytes(color)
end
ChunkyPNG::Chunk::Palette.new("PLTE", colors.pack("C*"))
end
# Determines the most suitable colormode for this palette.
# @return [Integer] The colormode which would create the smallest possible
# file for images that use this exact palette.
def best_color_settings
if black_and_white?
[ChunkyPNG::COLOR_GRAYSCALE, 1]
elsif grayscale?
if opaque?
[ChunkyPNG::COLOR_GRAYSCALE, 8]
else
[ChunkyPNG::COLOR_GRAYSCALE_ALPHA, 8]
end
elsif indexable?
[ChunkyPNG::COLOR_INDEXED, determine_bit_depth]
elsif opaque?
[ChunkyPNG::COLOR_TRUECOLOR, 8]
else
[ChunkyPNG::COLOR_TRUECOLOR_ALPHA, 8]
end
end
# Determines the minimal bit depth required for an indexed image
# @return [Integer] Number of bits per pixel, i.e. 1, 2, 4 or 8, or nil if
# this image cannot be saved as an indexed image.
def determine_bit_depth
case size
when 1..2 then 1
when 3..4 then 2
when 5..16 then 4
when 17..256 then 8
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/ 0000755 0001750 0001750 00000000000 13766004353 017415 5 ustar daniel daniel chunky_png-1.3.15/lib/chunky_png/canvas/data_url_exporting.rb 0000644 0001750 0001750 00000000716 13766004353 023640 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# Methods to export a canvas to a PNG data URL.
module DataUrlExporting
# Exports the canvas as a data url (e.g. data:image/png;base64,) that can
# easily be used inline in CSS or HTML.
# @return [String] The canvas formatted as a data URL string.
def to_data_url
["data:image/png;base64,", to_blob].pack("A*m").delete("\n")
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/data_url_importing.rb 0000644 0001750 0001750 00000001462 13766004353 023630 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# Methods to import a canvas from a PNG data URL.
module DataUrlImporting
# Imports a canvas from a PNG data URL.
# @param [String] string The data URL string to load from.
# @return [Canvas] The imported canvas.
# @raise ChunkyPNG::SignatureMismatch if the provides string is not a properly
# formatted PNG data URL (i.e. it should start with "data:image/png;base64,")
def from_data_url(string)
if string =~ %r[^data:image/png;base64,((?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$]
from_blob($1.unpack("m").first)
else
raise SignatureMismatch, "The string was not a properly formatted data URL for a PNG image."
end
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/resampling.rb 0000644 0001750 0001750 00000011520 13766004353 022102 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# The ChunkyPNG::Canvas::Resampling module defines methods to perform image resampling to
# a {ChunkyPNG::Canvas}.
#
# Currently, only the nearest neighbor algorithm is implemented. Bilinear and cubic
# algorithms may be added later on.
#
# @see ChunkyPNG::Canvas
module Resampling
# Integer Interpolation between two values
#
# Used for generating indicies for interpolation (eg, nearest
# neighbour).
#
# @param [Integer] width The width of the source
# @param [Integer] new_width The width of the destination
# @return [Array] An Array of Integer indicies
def steps(width, new_width)
indicies, residues = steps_residues(width, new_width)
for i in 1..new_width
indicies[i - 1] = (indicies[i - 1] + (residues[i - 1] + 127) / 255)
end
indicies
end
# Fractional Interpolation between two values
#
# Used for generating values for interpolation (eg, bilinear).
# Produces both the indices and the interpolation factors (residues).
#
# @param [Integer] width The width of the source
# @param [Integer] new_width The width of the destination
# @return [Array, Array] Two arrays of indicies and residues
def steps_residues(width, new_width)
indicies = Array.new(new_width, nil)
residues = Array.new(new_width, nil)
# This works by accumulating the fractional error and
# overflowing when necessary.
# We use mixed number arithmetic with a denominator of
# 2 * new_width
base_step = width / new_width
err_step = (width % new_width) << 1
denominator = new_width << 1
# Initial pixel
index = (width - new_width) / denominator
err = (width - new_width) % denominator
for i in 1..new_width
indicies[i - 1] = index
residues[i - 1] = (255.0 * err.to_f / denominator.to_f).round
index += base_step
err += err_step
if err >= denominator
index += 1
err -= denominator
end
end
[indicies, residues]
end
# Resamples the canvas using nearest neighbor interpolation.
# @param [Integer] new_width The width of the resampled canvas.
# @param [Integer] new_height The height of the resampled canvas.
# @return [ChunkyPNG::Canvas] A new canvas instance with the resampled pixels.
def resample_nearest_neighbor!(new_width, new_height)
steps_x = steps(width, new_width)
steps_y = steps(height, new_height)
pixels = Array.new(new_width * new_height)
i = 0
for y in steps_y
for x in steps_x
pixels[i] = get_pixel(x, y)
i += 1
end
end
replace_canvas!(new_width.to_i, new_height.to_i, pixels)
end
def resample_nearest_neighbor(new_width, new_height)
dup.resample_nearest_neighbor!(new_width, new_height)
end
# Resamples the canvas with bilinear interpolation.
# @param [Integer] new_width The width of the resampled canvas.
# @param [Integer] new_height The height of the resampled canvas.
# @return [ChunkyPNG::Canvas] A new canvas instance with the resampled pixels.
def resample_bilinear!(new_width, new_height)
index_x, interp_x = steps_residues(width, new_width)
index_y, interp_y = steps_residues(height, new_height)
pixels = Array.new(new_width * new_height)
i = 0
for y in 1..new_height
# Clamp the indicies to the edges of the image
y1 = [index_y[y - 1], 0].max
y2 = [index_y[y - 1] + 1, height - 1].min
y_residue = interp_y[y - 1]
for x in 1..new_width
# Clamp the indicies to the edges of the image
x1 = [index_x[x - 1], 0].max
x2 = [index_x[x - 1] + 1, width - 1].min
x_residue = interp_x[x - 1]
pixel_11 = get_pixel(x1, y1)
pixel_21 = get_pixel(x2, y1)
pixel_12 = get_pixel(x1, y2)
pixel_22 = get_pixel(x2, y2)
# Interpolate by Row
pixel_top = ChunkyPNG::Color.interpolate_quick(pixel_21, pixel_11, x_residue)
pixel_bot = ChunkyPNG::Color.interpolate_quick(pixel_22, pixel_12, x_residue)
# Interpolate by Column
pixels[i] = ChunkyPNG::Color.interpolate_quick(pixel_bot, pixel_top, y_residue)
i += 1
end
end
replace_canvas!(new_width.to_i, new_height.to_i, pixels)
end
def resample_bilinear(new_width, new_height)
dup.resample_bilinear!(new_width, new_height)
end
alias resample resample_nearest_neighbor
alias resize resample
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/adam7_interlacing.rb 0000644 0001750 0001750 00000006577 13766004353 023331 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# Methods for decoding and encoding Adam7 interlacing.
#
# Adam7 interlacing extracts 7 pass images out of a single image, that can be encoded to a
# stream separately so the image can be built up progressively. The module is included into
# ChunkyPNG canvas and is used to extract the pass images from the original image, or to
# reconstruct an original image from separate pass images.
module Adam7Interlacing
# Returns an array with the x-shift, x-offset, y-shift and y-offset for the requested pass.
# @param [Integer] pass The pass number, should be in 0..6.
def adam7_multiplier_offset(pass)
[
3 - (pass >> 1),
pass & 1 == 0 ? 0 : 8 >> ((pass + 1) >> 1),
pass == 0 ? 3 : 3 - ((pass - 1) >> 1),
pass == 0 || pass & 1 == 1 ? 0 : 8 >> (pass >> 1),
]
end
# Returns the pixel dimensions of the requested pass.
# @param [Integer] pass The pass number, should be in 0..6.
# @param [Integer] original_width The width of the original image.
# @param [Integer] original_height The height of the original image.
def adam7_pass_size(pass, original_width, original_height)
x_shift, x_offset, y_shift, y_offset = adam7_multiplier_offset(pass)
[
(original_width - x_offset + (1 << x_shift) - 1) >> x_shift,
(original_height - y_offset + (1 << y_shift) - 1) >> y_shift,
]
end
# Returns an array of the dimension of all the pass images.
# @param [Integer] original_width The width of the original image.
# @param [Integer] original_height The height of the original image.
# @return [Array>] Returns an array with 7 pairs of dimensions.
# @see #adam7_pass_size
def adam7_pass_sizes(original_width, original_height)
(0...7).map { |pass| adam7_pass_size(pass, original_width, original_height) }
end
# Merges a pass image into a total image that is being constructed.
# @param [Integer] pass The pass number, should be in 0..6.
# @param [ChunkyPNG::Canvas] canvas The image that is being constructed.
# @param [ChunkyPNG::Canvas] subcanvas The pass image that should be merged
def adam7_merge_pass(pass, canvas, subcanvas)
x_shift, x_offset, y_shift, y_offset = adam7_multiplier_offset(pass)
for y in 0...subcanvas.height do
for x in 0...subcanvas.width do
new_x = (x << x_shift) | x_offset
new_y = (y << y_shift) | y_offset
canvas[new_x, new_y] = subcanvas[x, y]
end
end
end
# Extracts a pass from a complete image
# @param [Integer] pass The pass number, should be in 0..6.
# @param [ChunkyPNG::Canvas] canvas The image that is being deconstructed.
# @return [ChunkyPNG::Canvas] The extracted pass image.
def adam7_extract_pass(pass, canvas)
x_shift, x_offset, y_shift, y_offset = adam7_multiplier_offset(pass)
sm_pixels = []
y_offset.step(canvas.height - 1, 1 << y_shift) do |y|
x_offset.step(canvas.width - 1, 1 << x_shift) do |x|
sm_pixels << canvas[x, y]
end
end
new_canvas_args = adam7_pass_size(pass, canvas.width, canvas.height) + [sm_pixels]
ChunkyPNG::Canvas.new(*new_canvas_args)
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/masking.rb 0000644 0001750 0001750 00000011343 13766004353 021375 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# The ChunkyPNG::Canvas::Masking module defines methods to perform masking
# and theming operations on a {ChunkyPNG::Canvas}. The module is included into the Canvas class so all
# these methods are available on every canvas.
#
# @see ChunkyPNG::Canvas
module Masking
# Creates a new image, based on the current image but with a new theme color.
#
# This method will replace one color in an image with another image. This is done by
# first extracting the pixels with a color close to the original theme color as a mask
# image, changing the color of this mask image and then apply it on the original image.
#
# Mask extraction works best when the theme colored pixels are clearly distinguishable
# from a background color (preferably white). You can set a tolerance level to influence
# the extraction process.
#
# @param [Integer] old_theme_color The original theme color in this image.
# @param [Integer] new_theme_color The color to replace the old theme color with.
# @param [Integer] bg_color The background color on which the theme colored pixels are placed.
# @param [Integer] tolerance The tolerance level to use when extracting the mask image. Five is
# the default; increase this if the masked image does not extract all the required pixels,
# decrease it if too many pixels get extracted.
# @return [ChunkyPNG::Canvas] Returns itself, but with the theme colored pixels changed.
# @see #change_theme_color!
# @see #change_mask_color!
def change_theme_color!(old_theme_color, new_theme_color, bg_color = ChunkyPNG::Color::WHITE, tolerance = 5)
base, mask = extract_mask(old_theme_color, bg_color, tolerance)
mask.change_mask_color!(new_theme_color)
replace!(base.compose!(mask))
end
# Creates a base image and a mask image from an original image that has a particular theme color.
# This can be used to easily change a theme color in an image.
#
# It will extract all the pixels that look like the theme color (with a tolerance level) and put
# these in a mask image. All the other pixels will be stored in a base image. Both images will be
# of the exact same size as the original image. The original image will be left untouched.
#
# The color of the mask image can be changed with {#change_mask_color!}. This new mask image can
# then be composed upon the base image to create an image with a new theme color. A call to
# {#change_theme_color!} will perform this in one go.
#
# @param [Integer] mask_color The current theme color.
# @param [Integer] bg_color The background color on which the theme colored pixels are applied.
# @param [Integer] tolerance The tolerance level to use when extracting the mask image. Five is
# the default; increase this if the masked image does not extract all the required pixels,
# decrease it if too many pixels get extracted.
# @return [Array] An array with the base canvas and the mask
# canvas as elements.
# @see #change_theme_color!
# @see #change_mask_color!
def extract_mask(mask_color, bg_color = ChunkyPNG::Color::WHITE, tolerance = 5)
base_pixels = []
mask_pixels = []
pixels.each do |pixel|
if ChunkyPNG::Color.alpha_decomposable?(pixel, mask_color, bg_color, tolerance)
mask_pixels << ChunkyPNG::Color.decompose_color(pixel, mask_color, bg_color, tolerance)
base_pixels << bg_color
else
mask_pixels << (mask_color & 0xffffff00)
base_pixels << pixel
end
end
[self.class.new(width, height, base_pixels), self.class.new(width, height, mask_pixels)]
end
# Changes the color of a mask image.
#
# This method works on a canvas extracted out of another image using the {#extract_mask} method.
# It can then be applied on the extracted base image. See {#change_theme_color!} to perform
# these operations in one go.
#
# @param [Integer] new_color The color to replace the original mask color with.
# @raise [ChunkyPNG::ExpectationFailed] when this canvas is not a mask image, i.e. its palette
# has more than once color, disregarding transparency.
# @see #change_theme_color!
# @see #extract_mask
def change_mask_color!(new_color)
raise ChunkyPNG::ExpectationFailed, "This is not a mask image!" if palette.opaque_palette.size != 1
pixels.map! { |pixel| (new_color & 0xffffff00) | ChunkyPNG::Color.a(pixel) }
self
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/stream_importing.rb 0000644 0001750 0001750 00000007711 13766004353 023333 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# Methods to quickly load a canvas from a stream, encoded in RGB, RGBA, BGR or ABGR format.
module StreamImporting
# Creates a canvas by reading pixels from an RGB formatted stream with a
# provided with and height.
#
# Every pixel should be represented by 3 bytes in the stream, in the correct
# RGB order. This format closely resembles the internal representation of a
# canvas object, so this kind of stream can be read extremely quickly.
#
# @param [Integer] width The width of the new canvas.
# @param [Integer] height The height of the new canvas.
# @param [#read, String] stream The stream to read the pixel data from.
# @return [ChunkyPNG::Canvas] The newly constructed canvas instance.
def from_rgb_stream(width, height, stream)
string = stream.respond_to?(:read) ? stream.read(3 * width * height) : stream.to_s[0, 3 * width * height]
string << ChunkyPNG::EXTRA_BYTE # Add a fourth byte to the last RGB triple.
unpacker = "NX" * (width * height)
pixels = string.unpack(unpacker).map { |color| color | 0x000000ff }
new(width, height, pixels)
end
# Creates a canvas by reading pixels from an RGBA formatted stream with a
# provided with and height.
#
# Every pixel should be represented by 4 bytes in the stream, in the correct
# RGBA order. This format is exactly like the internal representation of a
# canvas object, so this kind of stream can be read extremely quickly.
#
# @param [Integer] width The width of the new canvas.
# @param [Integer] height The height of the new canvas.
# @param [#read, String] stream The stream to read the pixel data from.
# @return [ChunkyPNG::Canvas] The newly constructed canvas instance.
def from_rgba_stream(width, height, stream)
string = stream.respond_to?(:read) ? stream.read(4 * width * height) : stream.to_s[0, 4 * width * height]
new(width, height, string.unpack("N*"))
end
# Creates a canvas by reading pixels from an BGR formatted stream with a
# provided with and height.
#
# Every pixel should be represented by 3 bytes in the stream, in the correct
# BGR order. This format closely resembles the internal representation of a
# canvas object, so this kind of stream can be read extremely quickly.
#
# @param [Integer] width The width of the new canvas.
# @param [Integer] height The height of the new canvas.
# @param [#read, String] stream The stream to read the pixel data from.
# @return [ChunkyPNG::Canvas] The newly constructed canvas instance.
def from_bgr_stream(width, height, stream)
string = ChunkyPNG::EXTRA_BYTE.dup # Add a first byte to the first BGR triple.
string << (stream.respond_to?(:read) ? stream.read(3 * width * height) : stream.to_s[0, 3 * width * height])
pixels = string.unpack("@1#{"XV" * (width * height)}").map { |color| color | 0x000000ff }
new(width, height, pixels)
end
# Creates a canvas by reading pixels from an ARGB formatted stream with a
# provided with and height.
#
# Every pixel should be represented by 4 bytes in the stream, in the correct
# ARGB order. This format is almost like the internal representation of a
# canvas object, so this kind of stream can be read extremely quickly.
#
# @param [Integer] width The width of the new canvas.
# @param [Integer] height The height of the new canvas.
# @param [#read, String] stream The stream to read the pixel data from.
# @return [ChunkyPNG::Canvas] The newly constructed canvas instance.
def from_abgr_stream(width, height, stream)
string = stream.respond_to?(:read) ? stream.read(4 * width * height) : stream.to_s[0, 4 * width * height]
new(width, height, string.unpack("V*"))
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/operations.rb 0000644 0001750 0001750 00000036617 13766004353 022142 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# The ChunkyPNG::Canvas::Operations module defines methods to perform
# operations on a {ChunkyPNG::Canvas}. The module is included into the
# Canvas class so all these methods are available on every canvas.
#
# Note that some of these operations modify the canvas, while some
# operations return a new canvas and leave the original intact.
#
# @see ChunkyPNG::Canvas
module Operations
# Converts the canvas to grayscale.
#
# This method will modify the canvas. The obtain a new canvas and leave
# the current instance intact, use {#grayscale} instead.
#
# @return [ChunkyPNG::Canvas] Returns itself, converted to grayscale.
# @see #grayscale
# @see ChunkyPNG::Color#to_grayscale
def grayscale!
pixels.map! { |pixel| ChunkyPNG::Color.to_grayscale(pixel) }
self
end
# Converts the canvas to grayscale, returning a new canvas.
#
# This method will not modify the canvas. To modift the current canvas,
# use {#grayscale!} instead.
#
# @return [ChunkyPNG::Canvas] A copy of the canvas, converted to
# grayscale.
# @see #grayscale!
# @see ChunkyPNG::Color#to_grayscale
def grayscale
dup.grayscale!
end
# Composes another image onto this image using alpha blending. This will
# modify the current canvas.
#
# If you simply want to replace pixels or when the other image does not
# have transparency, it is faster to use {#replace!}.
#
# @param [ChunkyPNG::Canvas] other The foreground canvas to compose on
# the current canvas, using alpha compositing.
# @param [Integer] offset_x The x-offset to apply the new foreground on.
# @param [Integer] offset_y The y-offset to apply the new foreground on.
# @return [ChunkyPNG::Canvas] Returns itself, but with the other canvas
# composed onto it.
# @raise [ChunkyPNG::OutOfBounds] when the other canvas doesn't fit on
# this one, given the offset and size of the other canvas.
# @see #replace!
# @see #compose
def compose!(other, offset_x = 0, offset_y = 0)
check_size_constraints!(other, offset_x, offset_y)
for y in 0...other.height do
for x in 0...other.width do
set_pixel(
x + offset_x,
y + offset_y,
ChunkyPNG::Color.compose(
other.get_pixel(x, y),
get_pixel(x + offset_x, y + offset_y)
)
)
end
end
self
end
# Composes another image onto this image using alpha blending. This will
# return a new canvas and leave the original intact.
#
# If you simply want to replace pixels or when the other image does not
# have transparency, it is faster to use {#replace}.
#
# @param (see #compose!)
# @return [ChunkyPNG::Canvas] Returns the new canvas, composed of the
# other 2.
# @raise [ChunkyPNG::OutOfBounds] when the other canvas doesn't fit on
# this one, given the offset and size of the other canvas.
#
# @note API changed since 1.0 - This method now no longer is in place,
# but returns a new canvas and leaves the original intact. Use
# {#compose!} if you want to compose on the canvas in place.
# @see #replace
def compose(other, offset_x = 0, offset_y = 0)
dup.compose!(other, offset_x, offset_y)
end
# Replaces pixels on this image by pixels from another pixels, on a given
# offset. This method will modify the current canvas.
#
# This will completely replace the pixels of the background image. If you
# want to blend them with semi-transparent pixels from the foreground
# image, see {#compose!}.
#
# @param [ChunkyPNG::Canvas] other The foreground canvas to get the
# pixels from.
# @param [Integer] offset_x The x-offset to apply the new foreground on.
# @param [Integer] offset_y The y-offset to apply the new foreground on.
# @return [ChunkyPNG::Canvas] Returns itself, but with the other canvas
# placed onto it.
# @raise [ChunkyPNG::OutOfBounds] when the other canvas doesn't fit on
# this one, given the offset and size of the other canvas.
# @see #compose!
# @see #replace
def replace!(other, offset_x = 0, offset_y = 0)
check_size_constraints!(other, offset_x, offset_y)
for y in 0...other.height do
for d in 0...other.width
pixels[(y + offset_y) * width + offset_x + d] = other.pixels[y * other.width + d]
end
end
self
end
# Replaces pixels on this image by pixels from another pixels, on a given
# offset. This method will modify the current canvas.
#
# This will completely replace the pixels of the background image. If you
# want to blend them with semi-transparent pixels from the foreground
# image, see {#compose!}.
#
# @param (see #replace!)
# @return [ChunkyPNG::Canvas] Returns a new, combined canvas.
# @raise [ChunkyPNG::OutOfBounds] when the other canvas doesn't fit on
# this one, given the offset and size of the other canvas.
#
# @note API changed since 1.0 - This method now no longer is in place,
# but returns a new canvas and leaves the original intact. Use
# {#replace!} if you want to replace pixels on the canvas in place.
# @see #compose
def replace(other, offset_x = 0, offset_y = 0)
dup.replace!(other, offset_x, offset_y)
end
# Crops an image, given the coordinates and size of the image that needs
# to be cut out. This will leave the original image intact and return a
# new, cropped image with pixels copied from the original image.
#
# @param [Integer] x The x-coordinate of the top left corner of the image
# to be cropped.
# @param [Integer] y The y-coordinate of the top left corner of the image
# to be cropped.
# @param [Integer] crop_width The width of the image to be cropped.
# @param [Integer] crop_height The height of the image to be cropped.
# @return [ChunkyPNG::Canvas] Returns the newly created cropped image.
# @raise [ChunkyPNG::OutOfBounds] when the crop dimensions plus the given
# coordinates are bigger then the original image.
def crop(x, y, crop_width, crop_height)
dup.crop!(x, y, crop_width, crop_height)
end
# Crops an image, given the coordinates and size of the image that needs
# to be cut out.
#
# This will change the size and content of the current canvas. Use
# {#crop} if you want to have a new canvas returned instead, leaving the
# current canvas intact.
#
# @param [Integer] x The x-coordinate of the top left corner of the image
# to be cropped.
# @param [Integer] y The y-coordinate of the top left corner of the image
# to be cropped.
# @param [Integer] crop_width The width of the image to be cropped.
# @param [Integer] crop_height The height of the image to be cropped.
# @return [ChunkyPNG::Canvas] Returns itself, but cropped.
# @raise [ChunkyPNG::OutOfBounds] when the crop dimensions plus the given
# coordinates are bigger then the original image.
def crop!(x, y, crop_width, crop_height)
if crop_width + x > width
raise ChunkyPNG::OutOfBounds, "Original image width is too small!"
end
if crop_height + y > height
raise ChunkyPNG::OutOfBounds, "Original image height is too small!"
end
if crop_width == width && x == 0
# We only need to crop off the top and/or bottom, so we can take a
# shortcut.
replace_canvas!(crop_width, crop_height, pixels.slice(y * width, width * crop_height))
else
new_pixels = []
for cy in 0...crop_height do
new_pixels.concat pixels.slice((cy + y) * width + x, crop_width)
end
replace_canvas!(crop_width, crop_height, new_pixels)
end
end
# Flips the image horizontally, leaving the original intact.
#
# This will flip the image on its horizontal axis, e.g. pixels on the top
# will now be pixels on the bottom. Chaining this method twice will
# return the original canvas. This method will leave the original object
# intact and return a new canvas.
#
# @return [ChunkyPNG::Canvas] The flipped image
# @see #flip_horizontally!
def flip_horizontally
dup.flip_horizontally!
end
# Flips the image horizontally in place.
#
# This will flip the image on its horizontal axis, e.g. pixels on the top
# will now be pixels on the bottom. Chaining this method twice will
# return the original canvas. This method will leave the original object
# intact and return a new canvas.
#
# @return [ChunkyPNG::Canvas] Itself, but flipped
# @see #flip_horizontally
def flip_horizontally!
for y in 0..((height - 1) >> 1) do
other_y = height - (y + 1)
other_row = row(other_y)
replace_row!(other_y, row(y))
replace_row!(y, other_row)
end
self
end
alias flip! flip_horizontally!
alias flip flip_horizontally
# Flips the image vertically, leaving the original intact.
#
# This will flip the image on its vertical axis, e.g. pixels on the left
# will now be pixels on the right. Chaining this method twice will return
# the original canvas. This method will leave the original object intact
# and return a new canvas.
#
# @return [ChunkyPNG::Canvas] The flipped image
# @see #flip_vertically!
def flip_vertically
dup.flip_vertically!
end
# Flips the image vertically in place.
#
# This will flip the image on its vertical axis, e.g. pixels on the left
# will now be pixels on the right. Chaining this method twice will return
# the original canvas. This method will leave the original object intact
# and return a new canvas.
#
# @return [ChunkyPNG::Canvas] Itself, but flipped
# @see #flip_vertically
def flip_vertically!
for y in 0...height do
replace_row!(y, row(y).reverse)
end
self
end
alias mirror! flip_vertically!
alias mirror flip_vertically
# Returns a new canvas instance that is rotated 90 degrees clockwise.
#
# This method will return a new canvas and leaves the original intact.
#
# @return [ChunkyPNG::Canvas] A clockwise-rotated copy.
# @see #rotate_right! for the in place version.
def rotate_right
dup.rotate_right!
end
# Rotates the image 90 degrees clockwise in place.
#
# This method will change the current canvas.
#
# @return [ChunkyPNG::Canvas] Itself, but rotated clockwise.
# @see #rotate_right for a version that leaves the current canvas intact
def rotate_right!
new_pixels = []
0.upto(width - 1) { |i| new_pixels += column(i).reverse }
replace_canvas!(height, width, new_pixels)
end
alias rotate_clockwise rotate_right
alias rotate_clockwise! rotate_right!
# Returns an image that is rotated 90 degrees counter-clockwise.
#
# This method will leave the original object intact and return a new
# canvas.
#
# @return [ChunkyPNG::Canvas] A rotated copy of itself.
# @see #rotate_left! for the in-place version.
def rotate_left
dup.rotate_left!
end
# Rotates the image 90 degrees counter-clockwise in place.
#
# This method will change the original canvas. See {#rotate_left} for a
# version that leaves the canvas intact and returns a new rotated canvas
# instead.
#
# @return [ChunkyPNG::Canvas] Itself, but rotated.
def rotate_left!
new_pixels = []
(width - 1).downto(0) { |i| new_pixels += column(i) }
replace_canvas!(height, width, new_pixels)
end
alias rotate_counter_clockwise rotate_left
alias rotate_counter_clockwise! rotate_left!
# Rotates the image 180 degrees.
#
# This method will leave the original object intact and return a new
# canvas.
#
# @return [ChunkyPNG::Canvas] The rotated image.
# @see #rotate_180!
def rotate_180
dup.rotate_180!
end
# Rotates the image 180 degrees in place.
#
# @return [ChunkyPNG::Canvas] Itself, but rotated 180 degrees.
# @see #rotate_180
def rotate_180!
pixels.reverse!
self
end
# Trims the border around the image, presumed to be the color of the
# first pixel.
#
# @param [Integer] border The color to attempt to trim.
# @return [ChunkyPNG::Canvas] The trimmed image.
# @see #trim!
def trim(border = pixels.first)
dup.trim!
end
# Trims the border around the image in place.
#
# @param [Integer] border The color to attempt to trim.
# @return [ChunkyPNG::Canvas] Returns itself, but with the border
# trimmed.
# @see #trim
def trim!(border = pixels.first)
x1 = [*0...width].index { |c| column(c).uniq != [border] }
x2 = [*0...width].rindex { |c| column(c).uniq != [border] }
y1 = [*0...height].index { |r| row(r).uniq != [border] }
y2 = [*0...height].rindex { |r| row(r).uniq != [border] }
crop! x1, y1, x2 - x1 + 1, y2 - y1 + 1
end
# Draws a border around the image.
#
# @param [Integer] size The size of the border.
# @param [Integer] color The color of the border.
# @return [ChunkyPNG::Canvas] Returns a bordered version of the image.
# @see #border!
def border(size, color = ChunkyPNG::Color::BLACK)
dup.border!(size, color)
end
# Draws a border around the image in place.
#
# @param [Integer] size The size of the border.
# @param [Integer] color The color of the border.
# @return [ChunkyPNG::Canvas] Returns itself with the border added.
# @see #border
def border!(size, color = ChunkyPNG::Color::BLACK)
new_width = width + size * 2
new_height = height + size * 2
bg = Canvas.new(new_width, new_height, color).replace(self, size, size)
replace_canvas!(new_width, new_height, bg.pixels)
end
protected
# Checks whether another image has the correct dimension to be used for
# an operation on the current image, given an offset coordinate to work
# with.
# @param [ChunkyPNG::Canvas] other The other canvas
# @param [Integer] offset_x The x offset on which the other image will be
# applied.
# @param [Integer] offset_y The y offset on which the other image will be
# applied.
# @raise [ChunkyPNG::OutOfBounds] when the other image doesn't fit.
def check_size_constraints!(other, offset_x, offset_y)
if width < other.width + offset_x
raise ChunkyPNG::OutOfBounds, "Background image width is too small!"
end
if height < other.height + offset_y
raise ChunkyPNG::OutOfBounds, "Background image height is too small!"
end
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/png_encoding.rb 0000644 0001750 0001750 00000052265 13766004353 022406 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# Methods for encoding a Canvas instance into a PNG datastream.
#
# Overview of the encoding process:
#
# * The image is split up in scanlines (i.e. rows of pixels);
# * All pixels are encoded as a pixelstream, based on the color mode.
# * All the pixel bytes in the pixelstream are adjusted using a filtering
# method if one is specified.
# * Compress the resulting string using deflate compression.
# * Split compressed data over one or more PNG chunks.
# * These chunks should be embedded in a datastream with at least a IHDR and
# IEND chunk and possibly a PLTE chunk.
#
# For interlaced images, the initial image is first split into 7 subimages.
# These images get encoded exactly as above, and the result gets combined
# before the compression step.
#
# @see ChunkyPNG::Canvas::PNGDecoding
# @see https://www.w3.org/TR/PNG/ The W3C PNG format specification
module PNGEncoding
# The palette used for encoding the image.This is only in used for images
# that get encoded using indexed colors.
# @return [ChunkyPNG::Palette]
attr_accessor :encoding_palette
# Writes the canvas to an IO stream, encoded as a PNG image.
# @param [IO] io The output stream to write to.
# @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream)
# @return [void]
def write(io, constraints = {})
to_datastream(constraints).write(io)
end
# Writes the canvas to a file, encoded as a PNG image.
# @param [String] filename The file to save the PNG image to.
# @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream)
# @return [void]
def save(filename, constraints = {})
File.open(filename, "wb") { |io| write(io, constraints) }
end
# Encoded the canvas to a PNG formatted string.
# @param constraints (see ChunkyPNG::Canvas::PNGEncoding#to_datastream)
# @return [String] The PNG encoded canvas as string.
def to_blob(constraints = {})
to_datastream(constraints).to_blob
end
alias to_string to_blob
alias to_s to_blob
# Converts this Canvas to a datastream, so that it can be saved as a PNG image.
# @param [Hash, Symbol] constraints The constraints to use when encoding the canvas.
# This can either be a hash with different constraints, or a symbol which acts as a
# preset for some constraints. If no constraints are given, ChunkyPNG will decide
# for itself how to best create the PNG datastream.
# Supported presets are :fast_rgba for quickly saving images with transparency,
# :fast_rgb for quickly saving opaque images, and :best_compression to
# obtain the smallest possible filesize.
# @option constraints [Fixnum] :color_mode The color mode to use. Use one of the
# ChunkyPNG::COLOR_* constants.
# @option constraints [true, false] :interlace Whether to use interlacing.
# @option constraints [Fixnum] :compression The compression level for Zlib. This can be a
# value between 0 and 9, or a Zlib constant like Zlib::BEST_COMPRESSION.
# @option constraints [Fixnum] :bit_depth The bit depth to use. This option is only used
# for indexed images, in which case it overrides the determined minimal bit depth. For
# all the other color modes, a bit depth of 8 is used.
# @return [ChunkyPNG::Datastream] The PNG datastream containing the encoded canvas.
# @see ChunkyPNG::Canvas::PNGEncoding#determine_png_encoding
def to_datastream(constraints = {})
encoding = determine_png_encoding(constraints)
ds = Datastream.new
ds.header_chunk = Chunk::Header.new(
width: width,
height: height,
color: encoding[:color_mode],
depth: encoding[:bit_depth],
interlace: encoding[:interlace]
)
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
ds.palette_chunk = encoding_palette.to_plte_chunk
ds.transparency_chunk = encoding_palette.to_trns_chunk unless encoding_palette.opaque?
end
data = encode_png_pixelstream(encoding[:color_mode], encoding[:bit_depth], encoding[:interlace], encoding[:filtering])
ds.data_chunks = Chunk::ImageData.split_in_chunks(data, encoding[:compression])
ds.end_chunk = Chunk::End.new
ds
end
protected
# Determines the best possible PNG encoding variables for this image, by analyzing
# the colors used for the image.
#
# You can provide constraints for the encoding variables by passing a hash with
# encoding variables to this method.
#
# @param [Hash, Symbol] constraints The constraints for the encoding. This can be a
# Hash or a preset symbol.
# @return [Hash] A hash with encoding options for {ChunkyPNG::Canvas::PNGEncoding#to_datastream}
def determine_png_encoding(constraints = {})
encoding = case constraints
when :fast_rgb then {color_mode: ChunkyPNG::COLOR_TRUECOLOR, compression: Zlib::BEST_SPEED}
when :fast_rgba then {color_mode: ChunkyPNG::COLOR_TRUECOLOR_ALPHA, compression: Zlib::BEST_SPEED}
when :best_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_PAETH}
when :good_compression then {compression: Zlib::BEST_COMPRESSION, filtering: ChunkyPNG::FILTER_NONE}
when :no_compression then {compression: Zlib::NO_COMPRESSION}
when :black_and_white then {color_mode: ChunkyPNG::COLOR_GRAYSCALE, bit_depth: 1}
when Hash then constraints
else raise ChunkyPNG::Exception, "Unknown encoding preset: #{constraints.inspect}"
end
# Do not create a palette when the encoding is given and does not require a palette.
if encoding[:color_mode]
if encoding[:color_mode] == ChunkyPNG::COLOR_INDEXED
self.encoding_palette = palette
encoding[:bit_depth] ||= encoding_palette.determine_bit_depth
else
encoding[:bit_depth] ||= 8
end
else
self.encoding_palette = palette
suggested_color_mode, suggested_bit_depth = encoding_palette.best_color_settings
encoding[:color_mode] ||= suggested_color_mode
encoding[:bit_depth] ||= suggested_bit_depth
end
# Use Zlib's default for compression unless otherwise provided.
encoding[:compression] ||= Zlib::DEFAULT_COMPRESSION
encoding[:interlace] = case encoding[:interlace]
when nil, false then ChunkyPNG::INTERLACING_NONE
when true then ChunkyPNG::INTERLACING_ADAM7
else encoding[:interlace]
end
encoding[:filtering] ||= case encoding[:compression]
when Zlib::BEST_COMPRESSION then ChunkyPNG::FILTER_PAETH
when Zlib::NO_COMPRESSION..Zlib::BEST_SPEED then ChunkyPNG::FILTER_NONE
else ChunkyPNG::FILTER_UP
end
encoding
end
# Encodes the canvas according to the PNG format specification with a given color
# mode, possibly with interlacing.
# @param [Integer] color_mode The color mode to use for encoding.
# @param [Integer] bit_depth The bit depth of the image.
# @param [Integer] interlace The interlacing method to use.
# @return [String] The PNG encoded canvas as string.
def encode_png_pixelstream(color_mode = ChunkyPNG::COLOR_TRUECOLOR, bit_depth = 8, interlace = ChunkyPNG::INTERLACING_NONE, filtering = ChunkyPNG::FILTER_NONE)
if color_mode == ChunkyPNG::COLOR_INDEXED
raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for encoding!" if encoding_palette.nil? || !encoding_palette.can_encode?
raise ChunkyPNG::ExpectationFailed, "This palette has too many colors!" if encoding_palette.size > (1 << bit_depth)
end
case interlace
when ChunkyPNG::INTERLACING_NONE then encode_png_image_without_interlacing(color_mode, bit_depth, filtering)
when ChunkyPNG::INTERLACING_ADAM7 then encode_png_image_with_interlacing(color_mode, bit_depth, filtering)
else raise ChunkyPNG::NotSupported, "Unknown interlacing method: #{interlace}!"
end
end
# Encodes the canvas according to the PNG format specification with a given color mode.
# @param [Integer] color_mode The color mode to use for encoding.
# @param [Integer] bit_depth The bit depth of the image.
# @param [Integer] filtering The filtering method to use.
# @return [String] The PNG encoded canvas as string.
def encode_png_image_without_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE)
stream = "".b
encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering)
stream
end
# Encodes the canvas according to the PNG format specification with a given color
# mode and Adam7 interlacing.
#
# This method will split the original canvas in 7 smaller canvases and encode them
# one by one, concatenating the resulting strings.
#
# @param [Integer] color_mode The color mode to use for encoding.
# @param [Integer] bit_depth The bit depth of the image.
# @param [Integer] filtering The filtering method to use.
# @return [String] The PNG encoded canvas as string.
def encode_png_image_with_interlacing(color_mode, bit_depth = 8, filtering = ChunkyPNG::FILTER_NONE)
stream = "".b
0.upto(6) do |pass|
subcanvas = self.class.adam7_extract_pass(pass, self)
subcanvas.encoding_palette = encoding_palette
subcanvas.encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering)
end
stream
end
# Encodes the canvas to a stream, in a given color mode.
# @param [String] stream The stream to write to.
# @param [Integer] color_mode The color mode to use for encoding.
# @param [Integer] bit_depth The bit depth of the image.
# @param [Integer] filtering The filtering method to use.
def encode_png_image_pass_to_stream(stream, color_mode, bit_depth, filtering)
start_pos = stream.bytesize
pixel_size = Color.pixel_bytesize(color_mode)
line_width = Color.scanline_bytesize(color_mode, bit_depth, width)
# Determine the filter method
encode_method = encode_png_pixels_to_scanline_method(color_mode, bit_depth)
filter_method = case filtering
when ChunkyPNG::FILTER_NONE then nil
when ChunkyPNG::FILTER_SUB then :encode_png_str_scanline_sub
when ChunkyPNG::FILTER_UP then :encode_png_str_scanline_up
when ChunkyPNG::FILTER_AVERAGE then :encode_png_str_scanline_average
when ChunkyPNG::FILTER_PAETH then :encode_png_str_scanline_paeth
else raise ArgumentError, "Filtering method #{filtering} is not supported"
end
0.upto(height - 1) do |y|
stream << send(encode_method, row(y))
end
# Now, apply filtering if any
if filter_method
(height - 1).downto(0) do |y|
pos = start_pos + y * (line_width + 1)
prev_pos = y == 0 ? nil : pos - (line_width + 1)
send(filter_method, stream, pos, prev_pos, line_width, pixel_size)
end
end
end
# Encodes a line of pixels using 8-bit truecolor mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_truecolor_8bit(pixels)
pixels.pack("x" + ("NX" * width))
end
# Encodes a line of pixels using 8-bit truecolor alpha mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_truecolor_alpha_8bit(pixels)
pixels.pack("xN#{width}")
end
# Encodes a line of pixels using 1-bit indexed mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_indexed_1bit(pixels)
chars = []
pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8|
chars << (
(encoding_palette.index(p1) << 7) |
(encoding_palette.index(p2) << 6) |
(encoding_palette.index(p3) << 5) |
(encoding_palette.index(p4) << 4) |
(encoding_palette.index(p5) << 3) |
(encoding_palette.index(p6) << 2) |
(encoding_palette.index(p7) << 1) |
encoding_palette.index(p8)
)
end
chars.pack("xC*")
end
# Encodes a line of pixels using 2-bit indexed mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_indexed_2bit(pixels)
chars = []
pixels.each_slice(4) do |p1, p2, p3, p4|
chars << (
(encoding_palette.index(p1) << 6) |
(encoding_palette.index(p2) << 4) |
(encoding_palette.index(p3) << 2) |
encoding_palette.index(p4)
)
end
chars.pack("xC*")
end
# Encodes a line of pixels using 4-bit indexed mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_indexed_4bit(pixels)
chars = []
pixels.each_slice(2) do |p1, p2|
chars << ((encoding_palette.index(p1) << 4) | encoding_palette.index(p2))
end
chars.pack("xC*")
end
# Encodes a line of pixels using 8-bit indexed mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_indexed_8bit(pixels)
pixels.map { |p| encoding_palette.index(p) }.pack("xC#{width}")
end
# Encodes a line of pixels using 1-bit grayscale mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_grayscale_1bit(pixels)
chars = []
pixels.each_slice(8) do |p1, p2, p3, p4, p5, p6, p7, p8|
chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 15 << 7) |
(p2.nil? ? 0 : (p2 & 0x0000ffff) >> 15 << 6) |
(p3.nil? ? 0 : (p3 & 0x0000ffff) >> 15 << 5) |
(p4.nil? ? 0 : (p4 & 0x0000ffff) >> 15 << 4) |
(p5.nil? ? 0 : (p5 & 0x0000ffff) >> 15 << 3) |
(p6.nil? ? 0 : (p6 & 0x0000ffff) >> 15 << 2) |
(p7.nil? ? 0 : (p7 & 0x0000ffff) >> 15 << 1) |
(p8.nil? ? 0 : (p8 & 0x0000ffff) >> 15))
end
chars.pack("xC*")
end
# Encodes a line of pixels using 2-bit grayscale mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_grayscale_2bit(pixels)
chars = []
pixels.each_slice(4) do |p1, p2, p3, p4|
chars << ((p1.nil? ? 0 : (p1 & 0x0000ffff) >> 14 << 6) |
(p2.nil? ? 0 : (p2 & 0x0000ffff) >> 14 << 4) |
(p3.nil? ? 0 : (p3 & 0x0000ffff) >> 14 << 2) |
(p4.nil? ? 0 : (p4 & 0x0000ffff) >> 14))
end
chars.pack("xC*")
end
# Encodes a line of pixels using 2-bit grayscale mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_grayscale_4bit(pixels)
chars = []
pixels.each_slice(2) do |p1, p2|
chars << ((p1.nil? ? 0 : ((p1 & 0x0000ffff) >> 12) << 4) | (p2.nil? ? 0 : ((p2 & 0x0000ffff) >> 12)))
end
chars.pack("xC*")
end
# Encodes a line of pixels using 8-bit grayscale mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_grayscale_8bit(pixels)
pixels.map { |p| p >> 8 }.pack("xC#{width}")
end
# Encodes a line of pixels using 8-bit grayscale alpha mode.
# @param [Array] pixels A row of pixels of the original image.
# @return [String] The encoded scanline as binary string
def encode_png_pixels_to_scanline_grayscale_alpha_8bit(pixels)
pixels.pack("xn#{width}")
end
# Returns the method name to use to decode scanlines into pixels.
# @param [Integer] color_mode The color mode of the image.
# @param [Integer] depth The bit depth of the image.
# @return [Symbol] The method name to use for decoding, to be called on the canvas class.
# @raise [ChunkyPNG::NotSupported] when the color_mode and/or bit depth is not supported.
def encode_png_pixels_to_scanline_method(color_mode, depth)
encoder_method = case color_mode
when ChunkyPNG::COLOR_TRUECOLOR then :"encode_png_pixels_to_scanline_truecolor_#{depth}bit"
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"encode_png_pixels_to_scanline_truecolor_alpha_#{depth}bit"
when ChunkyPNG::COLOR_INDEXED then :"encode_png_pixels_to_scanline_indexed_#{depth}bit"
when ChunkyPNG::COLOR_GRAYSCALE then :"encode_png_pixels_to_scanline_grayscale_#{depth}bit"
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"encode_png_pixels_to_scanline_grayscale_alpha_#{depth}bit"
end
raise ChunkyPNG::NotSupported, "No encoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(encoder_method, true)
encoder_method
end
# Encodes a scanline of a pixelstream without filtering. This is a no-op.
# @param [String] stream The pixelstream to work on. This string will be modified.
# @param [Integer] pos The starting position of the scanline.
# @param [Integer, nil] prev_pos The starting position of the previous scanline. nil if
# this is the first line.
# @param [Integer] line_width The number of bytes in this scanline, without counting the filtering
# method byte.
# @param [Integer] pixel_size The number of bytes used per pixel.
# @return [void]
def encode_png_str_scanline_none(stream, pos, prev_pos, line_width, pixel_size)
# noop - this method shouldn't get called at all.
end
# Encodes a scanline of a pixelstream using SUB filtering. This will modify the stream.
# @param (see #encode_png_str_scanline_none)
# @return [void]
def encode_png_str_scanline_sub(stream, pos, prev_pos, line_width, pixel_size)
line_width.downto(1) do |i|
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
stream.setbyte(pos + i, (stream.getbyte(pos + i) - a) & 0xff)
end
stream.setbyte(pos, ChunkyPNG::FILTER_SUB)
end
# Encodes a scanline of a pixelstream using UP filtering. This will modify the stream.
# @param (see #encode_png_str_scanline_none)
# @return [void]
def encode_png_str_scanline_up(stream, pos, prev_pos, line_width, pixel_size)
line_width.downto(1) do |i|
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
stream.setbyte(pos + i, (stream.getbyte(pos + i) - b) & 0xff)
end
stream.setbyte(pos, ChunkyPNG::FILTER_UP)
end
# Encodes a scanline of a pixelstream using AVERAGE filtering. This will modify the stream.
# @param (see #encode_png_str_scanline_none)
# @return [void]
def encode_png_str_scanline_average(stream, pos, prev_pos, line_width, pixel_size)
line_width.downto(1) do |i|
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
stream.setbyte(pos + i, (stream.getbyte(pos + i) - ((a + b) >> 1)) & 0xff)
end
stream.setbyte(pos, ChunkyPNG::FILTER_AVERAGE)
end
# Encodes a scanline of a pixelstream using PAETH filtering. This will modify the stream.
# @param (see #encode_png_str_scanline_none)
# @return [void]
def encode_png_str_scanline_paeth(stream, pos, prev_pos, line_width, pixel_size)
line_width.downto(1) do |i|
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0
p = a + b - c
pa = (p - a).abs
pb = (p - b).abs
pc = (p - c).abs
pr = if pa <= pb && pa <= pc
a
else
pb <= pc ? b : c
end
stream.setbyte(pos + i, (stream.getbyte(pos + i) - pr) & 0xff)
end
stream.setbyte(pos, ChunkyPNG::FILTER_PAETH)
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/stream_exporting.rb 0000644 0001750 0001750 00000004107 13766004353 023336 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# Methods to save load a canvas from to stream, encoded in RGB, RGBA, BGR or ABGR format.
module StreamExporting
# Creates an RGB-formatted pixelstream with the pixel data from this canvas.
#
# Note that this format is fast but bloated, because no compression is used
# and the internal representation is left intact. To reconstruct the
# canvas, the width and height should be known.
#
# @return [String] The RGBA-formatted pixel data.
def to_rgba_stream
pixels.pack("N*")
end
# Creates an RGB-formatted pixelstream with the pixel data from this canvas.
#
# Note that this format is fast but bloated, because no compression is used
# and the internal representation is almost left intact. To reconstruct
# the canvas, the width and height should be known.
#
# @return [String] The RGB-formatted pixel data.
def to_rgb_stream
pixels.pack("NX" * pixels.length)
end
# Creates a stream of the alpha channel of this canvas.
#
# @return [String] The 0-255 alpha values of all pixels packed as string
def to_alpha_channel_stream
pixels.pack("C*")
end
# Creates a grayscale stream of this canvas.
#
# This method assume sthat this image is fully grayscale, i.e. R = G = B for
# every pixel. The alpha channel will not be included in the stream.
#
# @return [String] The 0-255 grayscale values of all pixels packed as string.
def to_grayscale_stream
pixels.pack("nX" * pixels.length)
end
# Creates an ABGR-formatted pixelstream with the pixel data from this canvas.
#
# Note that this format is fast but bloated, because no compression is used
# and the internal representation is left intact. To reconstruct the
# canvas, the width and height should be known.
#
# @return [String] The RGBA-formatted pixel data.
def to_abgr_stream
pixels.pack("V*")
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/drawing.rb 0000644 0001750 0001750 00000027005 13766004353 021401 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# Module that adds some primitive drawing methods to {ChunkyPNG::Canvas}.
#
# All of these methods change the current canvas instance and do not create
# a new one, even though the method names do not end with a bang.
#
# @note Drawing operations will not fail when something is drawn outside of
# the bounds of the canvas; these pixels will simply be ignored.
# @see ChunkyPNG::Canvas
module Drawing
# Composes a pixel on the canvas by alpha blending a color with its
# background color.
#
# @param [Integer] x The x-coordinate of the pixel to blend.
# @param [Integer] y The y-coordinate of the pixel to blend.
# @param [Integer] color The foreground color to blend with
# @return [Integer] The composed color.
def compose_pixel(x, y, color)
return unless include_xy?(x, y)
compose_pixel_unsafe(x, y, ChunkyPNG::Color.parse(color))
end
# Composes a pixel on the canvas by alpha blending a color with its
# background color, without bounds checking.
#
# @param (see #compose_pixel)
# @return [Integer] The composed color.
def compose_pixel_unsafe(x, y, color)
set_pixel(x, y, ChunkyPNG::Color.compose(color, get_pixel(x, y)))
end
# Draws a Bezier curve
# @param [Array, Point] points A collection of control points
# @param [Integer] stroke_color
# @return [Chunky:PNG::Canvas] Itself, with the curve drawn
def bezier_curve(points, stroke_color = ChunkyPNG::Color::BLACK)
points = ChunkyPNG::Vector(*points)
case points.length
when 0, 1 then return self
when 2 then return line(points[0].x, points[0].y, points[1].x, points[1].y, stroke_color)
end
curve_points = []
t = 0
n = points.length - 1
while t <= 100
bicof = 0
cur_p = ChunkyPNG::Point.new(0, 0)
# Generate a float of t.
t_f = t / 100.00
cur_p.x += ((1 - t_f)**n) * points[0].x
cur_p.y += ((1 - t_f)**n) * points[0].y
for i in 1...points.length - 1
bicof = binomial_coefficient(n, i)
cur_p.x += (bicof * (1 - t_f)**(n - i)) * (t_f**i) * points[i].x
cur_p.y += (bicof * (1 - t_f)**(n - i)) * (t_f**i) * points[i].y
i += 1
end
cur_p.x += (t_f**n) * points[n].x
cur_p.y += (t_f**n) * points[n].y
curve_points << cur_p
t += 1
end
curve_points.each_cons(2) do |p1, p2|
line_xiaolin_wu(p1.x.round, p1.y.round, p2.x.round, p2.y.round, stroke_color)
end
self
end
# Draws an anti-aliased line using Xiaolin Wu's algorithm.
#
# @param [Integer] x0 The x-coordinate of the first control point.
# @param [Integer] y0 The y-coordinate of the first control point.
# @param [Integer] x1 The x-coordinate of the second control point.
# @param [Integer] y1 The y-coordinate of the second control point.
# @param [Integer] stroke_color The color to use for this line.
# @param [true, false] inclusive Whether to draw the last pixel. Set to
# false when drawing multiple lines in a path.
# @return [ChunkyPNG::Canvas] Itself, with the line drawn.
def line_xiaolin_wu(x0, y0, x1, y1, stroke_color, inclusive = true)
stroke_color = ChunkyPNG::Color.parse(stroke_color)
dx = x1 - x0
sx = dx < 0 ? -1 : 1
dx *= sx
dy = y1 - y0
sy = dy < 0 ? -1 : 1
dy *= sy
if dy == 0 # vertical line
x0.step(inclusive ? x1 : x1 - sx, sx) do |x|
compose_pixel(x, y0, stroke_color)
end
elsif dx == 0 # horizontal line
y0.step(inclusive ? y1 : y1 - sy, sy) do |y|
compose_pixel(x0, y, stroke_color)
end
elsif dx == dy # diagonal
x0.step(inclusive ? x1 : x1 - sx, sx) do |x|
compose_pixel(x, y0, stroke_color)
y0 += sy
end
elsif dy > dx # vertical displacement
compose_pixel(x0, y0, stroke_color)
e_acc = 0
e = ((dx << 16) / dy.to_f).round
(dy - 1).downto(0) do |i|
e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xffff
x0 += sx if e_acc <= e_acc_temp
w = 0xff - (e_acc >> 8)
compose_pixel(x0, y0, ChunkyPNG::Color.fade(stroke_color, w))
if inclusive || i > 0
compose_pixel(x0 + sx, y0 + sy, ChunkyPNG::Color.fade(stroke_color, 0xff - w))
end
y0 += sy
end
compose_pixel(x1, y1, stroke_color) if inclusive
else # horizontal displacement
compose_pixel(x0, y0, stroke_color)
e_acc = 0
e = ((dy << 16) / dx.to_f).round
(dx - 1).downto(0) do |i|
e_acc_temp, e_acc = e_acc, (e_acc + e) & 0xffff
y0 += sy if e_acc <= e_acc_temp
w = 0xff - (e_acc >> 8)
compose_pixel(x0, y0, ChunkyPNG::Color.fade(stroke_color, w))
if inclusive || i > 0
compose_pixel(x0 + sx, y0 + sy, ChunkyPNG::Color.fade(stroke_color, 0xff - w))
end
x0 += sx
end
compose_pixel(x1, y1, stroke_color) if inclusive
end
self
end
alias line line_xiaolin_wu
# Draws a polygon on the canvas using the stroke_color, filled using the
# fill_color if any.
#
# @param [Array, String] path The control point vector. Accepts everything
# {ChunkyPNG.Vector} accepts.
# @param [Integer] stroke_color The stroke color to use for this polygon.
# @param [Integer] fill_color The fill color to use for this polygon.
# @return [ChunkyPNG::Canvas] Itself, with the polygon drawn.
def polygon(path, stroke_color = ChunkyPNG::Color::BLACK, fill_color = ChunkyPNG::Color::TRANSPARENT)
vector = ChunkyPNG::Vector(*path)
if path.length < 3
raise ArgumentError, "A polygon requires at least 3 points"
end
stroke_color = ChunkyPNG::Color.parse(stroke_color)
fill_color = ChunkyPNG::Color.parse(fill_color)
# Fill
unless fill_color == ChunkyPNG::Color::TRANSPARENT
vector.y_range.each do |y|
intersections = []
vector.edges.each do |p1, p2|
if (p1.y < y && p2.y >= y) || (p2.y < y && p1.y >= y)
intersections << (p1.x + (y - p1.y).to_f / (p2.y - p1.y) * (p2.x - p1.x)).round
end
end
intersections.sort!
0.step(intersections.length - 1, 2) do |i|
intersections[i].upto(intersections[i + 1]) do |x|
compose_pixel(x, y, fill_color)
end
end
end
end
# Stroke
vector.each_edge do |(from_x, from_y), (to_x, to_y)|
line(from_x, from_y, to_x, to_y, stroke_color, false)
end
self
end
# Draws a rectangle on the canvas, using two control points.
#
# @param [Integer] x0 The x-coordinate of the first control point.
# @param [Integer] y0 The y-coordinate of the first control point.
# @param [Integer] x1 The x-coordinate of the second control point.
# @param [Integer] y1 The y-coordinate of the second control point.
# @param [Integer] stroke_color The line color to use for this rectangle.
# @param [Integer] fill_color The fill color to use for this rectangle.
# @return [ChunkyPNG::Canvas] Itself, with the rectangle drawn.
def rect(x0, y0, x1, y1, stroke_color = ChunkyPNG::Color::BLACK, fill_color = ChunkyPNG::Color::TRANSPARENT)
stroke_color = ChunkyPNG::Color.parse(stroke_color)
fill_color = ChunkyPNG::Color.parse(fill_color)
# Fill
unless fill_color == ChunkyPNG::Color::TRANSPARENT
[x0, x1].min.upto([x0, x1].max) do |x|
[y0, y1].min.upto([y0, y1].max) do |y|
compose_pixel(x, y, fill_color)
end
end
end
# Stroke
line(x0, y0, x0, y1, stroke_color, false)
line(x0, y1, x1, y1, stroke_color, false)
line(x1, y1, x1, y0, stroke_color, false)
line(x1, y0, x0, y0, stroke_color, false)
self
end
# Draws a circle on the canvas.
#
# @param [Integer] x0 The x-coordinate of the center of the circle.
# @param [Integer] y0 The y-coordinate of the center of the circle.
# @param [Integer] radius The radius of the circle from the center point.
# @param [Integer] stroke_color The color to use for the line.
# @param [Integer] fill_color The color to use that fills the circle.
# @return [ChunkyPNG::Canvas] Itself, with the circle drawn.
def circle(x0, y0, radius, stroke_color = ChunkyPNG::Color::BLACK, fill_color = ChunkyPNG::Color::TRANSPARENT)
stroke_color = ChunkyPNG::Color.parse(stroke_color)
fill_color = ChunkyPNG::Color.parse(fill_color)
f = 1 - radius
dd_f_x = 1
dd_f_y = -2 * radius
x = 0
y = radius
compose_pixel(x0, y0 + radius, stroke_color)
compose_pixel(x0, y0 - radius, stroke_color)
compose_pixel(x0 + radius, y0, stroke_color)
compose_pixel(x0 - radius, y0, stroke_color)
lines = [radius - 1] unless fill_color == ChunkyPNG::Color::TRANSPARENT
while x < y
if f >= 0
y -= 1
dd_f_y += 2
f += dd_f_y
end
x += 1
dd_f_x += 2
f += dd_f_x
unless fill_color == ChunkyPNG::Color::TRANSPARENT
lines[y] = lines[y] ? [lines[y], x - 1].min : x - 1
lines[x] = lines[x] ? [lines[x], y - 1].min : y - 1
end
compose_pixel(x0 + x, y0 + y, stroke_color)
compose_pixel(x0 - x, y0 + y, stroke_color)
compose_pixel(x0 + x, y0 - y, stroke_color)
compose_pixel(x0 - x, y0 - y, stroke_color)
unless x == y
compose_pixel(x0 + y, y0 + x, stroke_color)
compose_pixel(x0 - y, y0 + x, stroke_color)
compose_pixel(x0 + y, y0 - x, stroke_color)
compose_pixel(x0 - y, y0 - x, stroke_color)
end
end
unless fill_color == ChunkyPNG::Color::TRANSPARENT
lines.each_with_index do |length, y_offset|
if length > 0
line(x0 - length, y0 - y_offset, x0 + length, y0 - y_offset, fill_color)
end
if length > 0 && y_offset > 0
line(x0 - length, y0 + y_offset, x0 + length, y0 + y_offset, fill_color)
end
end
end
self
end
private
# Calculates the binomial coefficient for n over k.
#
# @param [Integer] n first parameter in coeffient (the number on top when
# looking at the mathematic formula)
# @param [Integer] k k-element, second parameter in coeffient (the number
# on the bottom when looking at the mathematic formula)
# @return [Integer] The binomial coeffcient of (n,k)
def binomial_coefficient(n, k)
return 1 if n == k || k == 0
return n if k == 1
return -1 if n < k
# calculate factorials
fact_n = (2..n).inject(1) { |carry, i| carry * i }
fact_k = (2..k).inject(1) { |carry, i| carry * i }
fact_n_sub_k = (2..(n - k)).inject(1) { |carry, i| carry * i }
fact_n / (fact_k * fact_n_sub_k)
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/canvas/png_decoding.rb 0000644 0001750 0001750 00000062232 13766004353 022367 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
class Canvas
# The PNGDecoding contains methods for decoding PNG datastreams to create a
# Canvas object. The datastream can be provided as filename, string or IO
# stream.
#
# Overview of the decoding process:
#
# * The optional PLTE and tRNS chunk are decoded for the color palette of
# the original image.
# * The contents of the IDAT chunks is combined, and uncompressed using
# Inflate decompression to the image pixelstream.
# * Based on the color mode, width and height of the original image, which
# is read from the PNG header (IHDR chunk), the amount of bytes
# per line is determined.
# * For every line of pixels in the encoded image, the original byte values
# are restored by unapplying the milter method for that line.
# * The read bytes are unfiltered given by the filter function specified by
# the first byte of the line.
# * The unfiltered pixelstream are is into colored pixels, using the color mode.
# * All lines combined to form the original image.
#
# For interlaced images, the original image was split into 7 subimages.
# These images get decoded just like the process above (from step 3), and get
# combined to form the original images.
#
# @see ChunkyPNG::Canvas::PNGEncoding
# @see https://www.w3.org/TR/PNG/ The W3C PNG format specification
module PNGDecoding
# Decodes a Canvas from a PNG encoded string.
# @param [String] str The string to read from.
# @return [ChunkyPNG::Canvas] The canvas decoded from the PNG encoded string.
def from_blob(str)
from_datastream(ChunkyPNG::Datastream.from_blob(str))
end
alias from_string from_blob
# Decodes a Canvas from a PNG encoded file.
# @param [String] filename The file to read from.
# @return [ChunkyPNG::Canvas] The canvas decoded from the PNG file.
def from_file(filename)
from_datastream(ChunkyPNG::Datastream.from_file(filename))
end
# Decodes a Canvas from a PNG encoded stream.
# @param [IO, #read] io The stream to read from.
# @return [ChunkyPNG::Canvas] The canvas decoded from the PNG stream.
def from_io(io)
from_datastream(ChunkyPNG::Datastream.from_io(io))
end
alias from_stream from_io
# Decodes the Canvas from a PNG datastream instance.
# @param [ChunkyPNG::Datastream] ds The datastream to decode.
# @return [ChunkyPNG::Canvas] The canvas decoded from the PNG datastream.
def from_datastream(ds)
width = ds.header_chunk.width
height = ds.header_chunk.height
color_mode = ds.header_chunk.color
interlace = ds.header_chunk.interlace
depth = ds.header_chunk.depth
if width == 0 || height == 0
raise ExpectationFailed, "Invalid image size, width: #{width}, height: #{height}"
end
decoding_palette, transparent_color = nil, nil
case color_mode
when ChunkyPNG::COLOR_INDEXED
decoding_palette = ChunkyPNG::Palette.from_chunks(ds.palette_chunk, ds.transparency_chunk)
when ChunkyPNG::COLOR_TRUECOLOR
transparent_color = ds.transparency_chunk.truecolor_entry(depth) if ds.transparency_chunk
when ChunkyPNG::COLOR_GRAYSCALE
transparent_color = ds.transparency_chunk.grayscale_entry(depth) if ds.transparency_chunk
end
decode_png_pixelstream(ds.imagedata, width, height, color_mode, depth, interlace, decoding_palette, transparent_color)
end
# Decodes a canvas from a PNG encoded pixelstream, using a given width, height,
# color mode and interlacing mode.
# @param [String] stream The pixelstream to read from.
# @param [Integer] width The width of the image.
# @param [Integer] height The height of the image.
# @param [Integer] color_mode The color mode of the encoded pixelstream.
# @param [Integer] depth The bit depth of the pixel samples.
# @param [Integer] interlace The interlace method of the encoded pixelstream.
# @param [ChunkyPNG::Palette] decoding_palette The palette to use to decode colors.
# @param [Integer] transparent_color The color that should be considered fully transparent.
# @return [ChunkyPNG::Canvas] The decoded Canvas instance.
def decode_png_pixelstream(stream, width, height, color_mode, depth, interlace, decoding_palette, transparent_color)
raise ChunkyPNG::ExpectationFailed, "This palette is not suitable for decoding!" if decoding_palette && !decoding_palette.can_decode?
image = case interlace
when ChunkyPNG::INTERLACING_NONE then decode_png_without_interlacing(stream, width, height, color_mode, depth, decoding_palette)
when ChunkyPNG::INTERLACING_ADAM7 then decode_png_with_adam7_interlacing(stream, width, height, color_mode, depth, decoding_palette)
else raise ChunkyPNG::NotSupported, "Don't know how the handle interlacing method #{interlace}!"
end
image.pixels.map! { |c| c == transparent_color ? ChunkyPNG::Color::TRANSPARENT : c } if transparent_color
image
end
protected
# Decodes a canvas from a non-interlaced PNG encoded pixelstream, using a
# given width, height and color mode.
# @param stream (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param width (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param height (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param color_mode (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param depth (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param [ChunkyPNG::Palette] decoding_palette The palette to use to decode colors.
# @return (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
def decode_png_without_interlacing(stream, width, height, color_mode, depth, decoding_palette)
decode_png_image_pass(stream, width, height, color_mode, depth, 0, decoding_palette)
end
# Decodes a canvas from a Adam 7 interlaced PNG encoded pixelstream, using a
# given width, height and color mode.
# @param stream (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param width (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param height (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param color_mode (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param depth (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param [ChunkyPNG::Palette] decoding_palette The palette to use to decode colors.
# @return (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
def decode_png_with_adam7_interlacing(stream, width, height, color_mode, depth, decoding_palette)
canvas = new(width, height)
start_pos = 0
for pass in 0...7
sm_width, sm_height = adam7_pass_size(pass, width, height)
sm = decode_png_image_pass(stream, sm_width, sm_height, color_mode, depth, start_pos, decoding_palette)
adam7_merge_pass(pass, canvas, sm)
start_pos += ChunkyPNG::Color.pass_bytesize(color_mode, depth, sm_width, sm_height)
end
canvas
end
# Extract 4 consecutive bits from a byte.
# @param [Integer] byte The byte (0..255) value to extract a 4 bit value from.
# @param [Integer] index The index within the byte. This should be either 0 or 2;
# the value will be modded by 2 to enforce this.
# @return [Integer] The extracted 4bit value (0..15)
def decode_png_extract_4bit_value(byte, index)
index & 0x01 == 0 ? ((byte & 0xf0) >> 4) : (byte & 0x0f)
end
# Extract 2 consecutive bits from a byte.
# @param [Integer] byte The byte (0..255) value to extract a 2 bit value from.
# @param [Integer] index The index within the byte. This should be either 0, 1, 2, or 3;
# the value will be modded by 4 to enforce this.
# @return [Integer] The extracted 2 bit value (0..3)
def decode_png_extract_2bit_value(byte, index)
bitshift = 6 - ((index & 0x03) << 1)
(byte & (0x03 << bitshift)) >> bitshift
end
# Extract a bit from a byte on a given index.
# @param [Integer] byte The byte (0..255) value to extract a bit from.
# @param [Integer] index The index within the byte. This should be 0..7;
# the value will be modded by 8 to enforce this.
# @return [Integer] Either 1 or 0.
def decode_png_extract_1bit_value(byte, index)
bitshift = 7 - (index & 0x07)
(byte & (0x01 << bitshift)) >> bitshift
end
# Resamples a 16 bit value to an 8 bit value. This will discard some color information.
# @param [Integer] value The 16 bit value to resample.
# @return [Integer] The 8 bit resampled value
def decode_png_resample_16bit_value(value)
value >> 8
end
# No-op - available for completeness sake only
# @param [Integer] value The 8 bit value to resample.
# @return [Integer] The 8 bit resampled value
def decode_png_resample_8bit_value(value)
value
end
# Resamples a 4 bit value to an 8 bit value.
# @param [Integer] value The 4 bit value to resample.
# @return [Integer] The 8 bit resampled value.
def decode_png_resample_4bit_value(value)
value << 4 | value
end
# Resamples a 2 bit value to an 8 bit value.
# @param [Integer] value The 2 bit value to resample.
# @return [Integer] The 8 bit resampled value.
def decode_png_resample_2bit_value(value)
value << 6 | value << 4 | value << 2 | value
end
# Resamples a 1 bit value to an 8 bit value.
# @param [Integer] value The 1 bit value to resample.
# @return [Integer] The 8 bit resampled value
def decode_png_resample_1bit_value(value)
value == 0x01 ? 0xff : 0x00
end
# Decodes a scanline of a 1-bit, indexed image into a row of pixels.
# @param [String] stream The stream to decode from.
# @param [Integer] pos The position in the stream on which the scanline starts (including the filter byte).
# @param [Integer] width The width in pixels of the scanline.
# @param [ChunkyPNG::Palette] decoding_palette The palette to use to decode colors.
# @return [Array] An array of decoded pixels.
def decode_png_pixels_from_scanline_indexed_1bit(stream, pos, width, decoding_palette)
(0...width).map do |index|
palette_pos = decode_png_extract_1bit_value(stream.getbyte(pos + 1 + (index >> 3)), index)
decoding_palette[palette_pos]
end
end
# Decodes a scanline of a 2-bit, indexed image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_indexed_2bit(stream, pos, width, decoding_palette)
(0...width).map do |index|
palette_pos = decode_png_extract_2bit_value(stream.getbyte(pos + 1 + (index >> 2)), index)
decoding_palette[palette_pos]
end
end
# Decodes a scanline of a 4-bit, indexed image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_indexed_4bit(stream, pos, width, decoding_palette)
(0...width).map do |index|
palette_pos = decode_png_extract_4bit_value(stream.getbyte(pos + 1 + (index >> 1)), index)
decoding_palette[palette_pos]
end
end
# Decodes a scanline of a 8-bit, indexed image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_indexed_8bit(stream, pos, width, decoding_palette)
(1..width).map { |i| decoding_palette[stream.getbyte(pos + i)] }
end
# Decodes a scanline of an 8-bit, true color image with transparency into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_truecolor_alpha_8bit(stream, pos, width, _decoding_palette)
stream.unpack("@#{pos + 1}N#{width}")
end
# Decodes a scanline of a 16-bit, true color image with transparency into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_truecolor_alpha_16bit(stream, pos, width, _decoding_palette)
pixels = []
stream.unpack("@#{pos + 1}n#{width * 4}").each_slice(4) do |r, g, b, a|
pixels << ChunkyPNG::Color.rgba(
decode_png_resample_16bit_value(r),
decode_png_resample_16bit_value(g),
decode_png_resample_16bit_value(b),
decode_png_resample_16bit_value(a),
)
end
pixels
end
# Decodes a scanline of an 8-bit, true color image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_truecolor_8bit(stream, pos, width, _decoding_palette)
stream.unpack("@#{pos + 1}#{"NX" * width}").map { |c| c | 0x000000ff }
end
# Decodes a scanline of a 16-bit, true color image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_truecolor_16bit(stream, pos, width, _decoding_palette)
pixels = []
stream.unpack("@#{pos + 1}n#{width * 3}").each_slice(3) do |r, g, b|
pixels << ChunkyPNG::Color.rgb(decode_png_resample_16bit_value(r), decode_png_resample_16bit_value(g), decode_png_resample_16bit_value(b))
end
pixels
end
# Decodes a scanline of an 8-bit, grayscale image with transparency into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_grayscale_alpha_8bit(stream, pos, width, _decoding_palette)
(0...width).map { |i| ChunkyPNG::Color.grayscale_alpha(stream.getbyte(pos + (i * 2) + 1), stream.getbyte(pos + (i * 2) + 2)) }
end
# Decodes a scanline of a 16-bit, grayscale image with transparency into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_grayscale_alpha_16bit(stream, pos, width, _decoding_palette)
pixels = []
stream.unpack("@#{pos + 1}n#{width * 2}").each_slice(2) do |g, a|
pixels << ChunkyPNG::Color.grayscale_alpha(decode_png_resample_16bit_value(g), decode_png_resample_16bit_value(a))
end
pixels
end
# Decodes a scanline of a 1-bit, grayscale image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_grayscale_1bit(stream, pos, width, _decoding_palette)
(0...width).map do |index|
value = decode_png_extract_1bit_value(stream.getbyte(pos + 1 + (index >> 3)), index)
value == 1 ? ChunkyPNG::Color::WHITE : ChunkyPNG::Color::BLACK
end
end
# Decodes a scanline of a 2-bit, grayscale image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_grayscale_2bit(stream, pos, width, _decoding_palette)
(0...width).map do |index|
value = decode_png_extract_2bit_value(stream.getbyte(pos + 1 + (index >> 2)), index)
ChunkyPNG::Color.grayscale(decode_png_resample_2bit_value(value))
end
end
# Decodes a scanline of a 4-bit, grayscale image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_grayscale_4bit(stream, pos, width, _decoding_palette)
(0...width).map do |index|
value = decode_png_extract_4bit_value(stream.getbyte(pos + 1 + (index >> 1)), index)
ChunkyPNG::Color.grayscale(decode_png_resample_4bit_value(value))
end
end
# Decodes a scanline of an 8-bit, grayscale image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_grayscale_8bit(stream, pos, width, _decoding_palette)
(1..width).map { |i| ChunkyPNG::Color.grayscale(stream.getbyte(pos + i)) }
end
# Decodes a scanline of a 16-bit, grayscale image into a row of pixels.
# @params (see #decode_png_pixels_from_scanline_indexed_1bit)
# @return (see #decode_png_pixels_from_scanline_indexed_1bit)
def decode_png_pixels_from_scanline_grayscale_16bit(stream, pos, width, _decoding_palette)
values = stream.unpack("@#{pos + 1}n#{width}")
values.map { |value| ChunkyPNG::Color.grayscale(decode_png_resample_16bit_value(value)) }
end
# Returns the method name to use to decode scanlines into pixels.
# @param [Integer] color_mode The color mode of the image.
# @param [Integer] depth The bit depth of the image.
# @return [Symbol] The method name to use for decoding, to be called on the canvas class.
# @raise [ChunkyPNG::NotSupported] when the color_mode and/or bit depth is not supported.
def decode_png_pixels_from_scanline_method(color_mode, depth)
decoder_method = case color_mode
when ChunkyPNG::COLOR_TRUECOLOR then :"decode_png_pixels_from_scanline_truecolor_#{depth}bit"
when ChunkyPNG::COLOR_TRUECOLOR_ALPHA then :"decode_png_pixels_from_scanline_truecolor_alpha_#{depth}bit"
when ChunkyPNG::COLOR_INDEXED then :"decode_png_pixels_from_scanline_indexed_#{depth}bit"
when ChunkyPNG::COLOR_GRAYSCALE then :"decode_png_pixels_from_scanline_grayscale_#{depth}bit"
when ChunkyPNG::COLOR_GRAYSCALE_ALPHA then :"decode_png_pixels_from_scanline_grayscale_alpha_#{depth}bit"
end
raise ChunkyPNG::NotSupported, "No decoder found for color mode #{color_mode} and #{depth}-bit depth!" unless respond_to?(decoder_method, true)
decoder_method
end
# Decodes a single PNG image pass width a given width, height and color
# mode, to a Canvas, starting at the given position in the stream.
#
# A non-interlaced image only consists of one pass, while an Adam7
# image consists of 7 passes that must be combined after decoding.
#
# @param stream (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param width (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param height (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param color_mode (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
# @param [Integer] start_pos The position in the pixel stream to start reading.
# @param [ChunkyPNG::Palette] decoding_palette The palette to use to decode colors.
# @return (see ChunkyPNG::Canvas::PNGDecoding#decode_png_pixelstream)
def decode_png_image_pass(stream, width, height, color_mode, depth, start_pos, decoding_palette)
pixels = []
if width > 0 && height > 0
stream << ChunkyPNG::EXTRA_BYTE if color_mode == ChunkyPNG::COLOR_TRUECOLOR
pixel_decoder = decode_png_pixels_from_scanline_method(color_mode, depth)
line_length = ChunkyPNG::Color.scanline_bytesize(color_mode, depth, width)
pixel_size = ChunkyPNG::Color.pixel_bytesize(color_mode, depth)
raise ChunkyPNG::ExpectationFailed, "Invalid stream length!" unless stream.bytesize - start_pos >= ChunkyPNG::Color.pass_bytesize(color_mode, depth, width, height)
pos, prev_pos = start_pos, nil
for _ in 0...height do
decode_png_str_scanline(stream, pos, prev_pos, line_length, pixel_size)
pixels.concat(send(pixel_decoder, stream, pos, width, decoding_palette))
prev_pos = pos
pos += line_length + 1
end
end
new(width, height, pixels)
end
# Decodes a scanline if it was encoded using filtering.
#
# It will extract the filtering method from the first byte of the scanline, and uses the
# method to change the subsequent bytes to unfiltered values. This will modify the pixelstream.
#
# The bytes of the scanline can then be used to construct pixels, based on the color mode..
#
# @param [String] stream The pixelstream to undo the filtering in.
# @param [Integer] pos The starting position of the scanline to decode.
# @param [Integer, nil] prev_pos The starting position of the previously decoded scanline, or nil
# if this is the first scanline of the image.
# @param [Integer] line_length The number of bytes in the scanline, discounting the filter method byte.
# @param [Integer] pixel_size The number of bytes used per pixel, based on the color mode.
# @return [void]
def decode_png_str_scanline(stream, pos, prev_pos, line_length, pixel_size)
case stream.getbyte(pos)
when ChunkyPNG::FILTER_NONE then # rubocop:disable Lint/EmptyWhen # no-op
when ChunkyPNG::FILTER_SUB then decode_png_str_scanline_sub(stream, pos, prev_pos, line_length, pixel_size)
when ChunkyPNG::FILTER_UP then decode_png_str_scanline_up(stream, pos, prev_pos, line_length, pixel_size)
when ChunkyPNG::FILTER_AVERAGE then decode_png_str_scanline_average(stream, pos, prev_pos, line_length, pixel_size)
when ChunkyPNG::FILTER_PAETH then decode_png_str_scanline_paeth(stream, pos, prev_pos, line_length, pixel_size)
else raise ChunkyPNG::NotSupported, "Unknown filter type: #{stream.getbyte(pos)}!"
end
end
# Decodes a scanline that wasn't encoded using filtering. This is a no-op.
# @params (see #decode_png_str_scanline)
# @return [void]
def decode_png_str_scanline_sub_none(stream, pos, prev_pos, line_length, pixel_size)
# noop - this method shouldn't get called.
end
# Decodes a scanline in a pixelstream that was encoded using SUB filtering.
# This will change the pixelstream to have unfiltered values.
# @params (see #decode_png_str_scanline)
# @return [void]
def decode_png_str_scanline_sub(stream, pos, prev_pos, line_length, pixel_size)
for i in 1..line_length do
stream.setbyte(pos + i, (stream.getbyte(pos + i) + (i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0)) & 0xff)
end
end
# Decodes a scanline in a pixelstream that was encoded using UP filtering.
# This will change the pixelstream to have unfiltered values.
# @params (see #decode_png_str_scanline)
# @return [void]
def decode_png_str_scanline_up(stream, pos, prev_pos, line_length, pixel_size)
for i in 1..line_length do
up = prev_pos ? stream.getbyte(prev_pos + i) : 0
stream.setbyte(pos + i, (stream.getbyte(pos + i) + up) & 0xff)
end
end
# Decodes a scanline in a pixelstream that was encoded using AVERAGE filtering.
# This will change the pixelstream to have unfiltered values.
# @params (see #decode_png_str_scanline)
# @return [void]
def decode_png_str_scanline_average(stream, pos, prev_pos, line_length, pixel_size)
for i in 1..line_length do
a = i > pixel_size ? stream.getbyte(pos + i - pixel_size) : 0
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
stream.setbyte(pos + i, (stream.getbyte(pos + i) + ((a + b) >> 1)) & 0xff)
end
end
# Decodes a scanline in a pixelstream that was encoded using PAETH filtering.
# This will change the pixelstream to have unfiltered values.
# @params (see #decode_png_str_scanline)
# @return [void]
def decode_png_str_scanline_paeth(stream, pos, prev_pos, line_length, pixel_size)
for i in 1..line_length do
cur_pos = pos + i
a = i > pixel_size ? stream.getbyte(cur_pos - pixel_size) : 0
b = prev_pos ? stream.getbyte(prev_pos + i) : 0
c = prev_pos && i > pixel_size ? stream.getbyte(prev_pos + i - pixel_size) : 0
p = a + b - c
pa = (p - a).abs
pb = (p - b).abs
pc = (p - c).abs
pr = if pa <= pb
pa <= pc ? a : c
else
pb <= pc ? b : c
end
stream.setbyte(cur_pos, (stream.getbyte(cur_pos) + pr) & 0xff)
end
end
end
end
end
chunky_png-1.3.15/lib/chunky_png/image.rb 0000644 0001750 0001750 00000005401 13766004353 017551 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# ChunkyPNG::Image is an extension of the {ChunkyPNG::Canvas} class, that
# also includes support for metadata.
#
# @see ChunkyPNG::Canvas
class Image < Canvas
# The minimum size of bytes the value of a metadata field should be before compression
# is enabled for the chunk.
METADATA_COMPRESSION_TRESHOLD = 300
# @return [Hash] The hash of metadata fields for this PNG image.
attr_accessor :metadata
# Initializes a new ChunkyPNG::Image instance.
# @param [Integer] width The width of the new image.
# @param [Integer] height The height of the new image.
# @param [Integer] bg_color The background color of the new image.
# @param [Hash] metadata A hash of metadata fields and values for this image.
# @see ChunkyPNG::Canvas#initialize
def initialize(width, height, bg_color = ChunkyPNG::Color::TRANSPARENT, metadata = {})
super(width, height, bg_color)
@metadata = metadata
end
# Initializes a copy of another ChunkyPNG::Image instance.
#
# @param [ChunkyPNG::Image] other The other image to copy.
def initialize_copy(other)
super(other)
@metadata = other.metadata
end
# Returns the metadata for this image as PNG chunks.
#
# Chunks will either be of the {ChunkyPNG::Chunk::Text} type for small
# values (in bytes), or of the {ChunkyPNG::Chunk::CompressedText} type
# for values that are larger in size.
#
# @return [Array] An array of metadata chunks.
# @see ChunkyPNG::Image::METADATA_COMPRESSION_TRESHOLD
def metadata_chunks
metadata.map do |key, value|
if value.length >= METADATA_COMPRESSION_TRESHOLD
ChunkyPNG::Chunk::CompressedText.new(key, value)
else
ChunkyPNG::Chunk::Text.new(key, value)
end
end
end
# Encodes the image to a PNG datastream for saving to disk or writing to an IO stream.
#
# Besides encoding the canvas, it will also encode the metadata fields to text chunks.
#
# @param [Hash] constraints The constraints to use when encoding the canvas.
# @return [ChunkyPNG::Datastream] The datastream that contains this image.
# @see ChunkyPNG::Canvas::PNGEncoding#to_datastream
# @see #metadata_chunks
def to_datastream(constraints = {})
ds = super(constraints)
ds.other_chunks += metadata_chunks
ds
end
# Reads a ChunkyPNG::Image instance from a data stream.
#
# Besides decoding the canvas, this will also read the metadata fields
# from the datastream.
#
# @param [ChunkyPNG::Datastream] ds The datastream to read from.
def self.from_datastream(ds)
image = super(ds)
image.metadata = ds.metadata
image
end
end
end
chunky_png-1.3.15/lib/chunky_png/vector.rb 0000644 0001750 0001750 00000015353 13766004353 020000 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# Factory method for {ChunkyPNG::Vector} instances.
#
# @overload Vector(x0, y0, x1, y1, x2, y2, ...)
# Creates a vector by parsing two subsequent values in the argument list
# as x- and y-coordinate of a point.
# @return [ChunkyPNG::Vector] The instantiated vector.
# @overload Vector(string)
# Creates a vector by parsing coordinates from the input string.
# @return [ChunkyPNG::Vector] The instantiated vector.
# @overload Vector(pointlike, pointlike, pointlike, ...)
# Creates a vector by converting every argument to a point using {ChunkyPNG.Point}.
# @return [ChunkyPNG::Vector] The instantiated vector.
#
# @return [ChunkyPNG::Vector] The vector created by this factory method.
# @raise [ArgumentError] If the given arguments could not be understood as a vector.
# @see ChunkyPNG::Vector
def self.Vector(*args)
return args.first if args.length == 1 && args.first.is_a?(ChunkyPNG::Vector)
if args.length == 1 && args.first.respond_to?(:scan)
ChunkyPNG::Vector.new(ChunkyPNG::Vector.multiple_from_string(args.first)) # e.g. ['1,1 2,2 3,3']
else
ChunkyPNG::Vector.new(ChunkyPNG::Vector.multiple_from_array(args)) # e.g. [[1,1], [2,2], [3,3]] or [1,1,2,2,3,3]
end
end
# Class that represents a vector of points, i.e. a list of {ChunkyPNG::Point} instances.
#
# Vectors can be created quite flexibly. See the {ChunkyPNG.Vector} factory methods for
# more information on how to construct vectors.
class Vector
include Enumerable
# @return [Array] The array that holds all the points in this vector.
attr_reader :points
# Initializes a vector based on a list of Point instances.
#
# You usually do not want to use this method directly, but call {ChunkyPNG.Vector} instead.
#
# @param [Array] points
# @see ChunkyPNG.Vector
def initialize(points = [])
@points = points
end
# Iterates over all the edges in this vector.
#
# An edge is a combination of two subsequent points in the vector. Together, they will form
# a path from the first point to the last point
#
# @param [true, false] close Whether to close the path, i.e. return an edge that connects the last
# point in the vector back to the first point.
# @return [void]
# @raise [ChunkyPNG::ExpectationFailed] if the vector contains less than two points.
# @see #edges
def each_edge(close = true)
raise ChunkyPNG::ExpectationFailed, "Not enough points in this path to draw an edge!" if length < 2
points.each_cons(2) { |a, b| yield(a, b) }
yield(points.last, points.first) if close
end
# Returns the point with the given indexof this vector.
# @param [Integer] index The 0-based index of the point in this vector.
# @return [ChunkyPNG::Point] The point instance.
def [](index)
points[index]
end
# Returns an enumerator that will iterate over all the edges in this vector.
# @param (see #each_edge)
# @return [Enumerator] The enumerator that iterates over the edges.
# @raise [ChunkyPNG::ExpectationFailed] if the vector contains less than two points.
# @see #each_edge
def edges(close = true)
to_enum(:each_edge, close)
end
# Returns the number of points in this vector.
# @return [Integer] The length of the points array.
def length
points.length
end
# Iterates over all the points in this vector
# @yield [ChunkyPNG::Point] The points in the correct order.
# @return [void]
def each(&block)
points.each(&block)
end
# Comparison between two vectors for quality.
# @param [ChunkyPNG::Vector] other The vector to compare with.
# @return [true, false] true if the list of points are identical
def eql?(other)
other.points == points
end
alias == eql?
# Returns the range in x-coordinates for all the points in this vector.
# @return [Range] The (inclusive) range of x-coordinates.
def x_range
Range.new(*points.map { |p| p.x }.minmax)
end
# Returns the range in y-coordinates for all the points in this vector.
# @return [Range] The (inclusive) range of y-coordinates.
def y_range
Range.new(*points.map { |p| p.y }.minmax)
end
# Finds the lowest x-coordinate in this vector.
# @return [Integer] The lowest x-coordinate of all the points in the vector.
def min_x
x_range.first
end
# Finds the highest x-coordinate in this vector.
# @return [Integer] The highest x-coordinate of all the points in the vector.
def max_x
x_range.last
end
# Finds the lowest y-coordinate in this vector.
# @return [Integer] The lowest y-coordinate of all the points in the vector.
def min_y
y_range.first
end
# Finds the highest y-coordinate in this vector.
# @return [Integer] The highest y-coordinate of all the points in the vector.
def max_y
y_range.last
end
# Returns the offset from (0,0) of the minimal bounding box of all the
# points in this vector
# @return [ChunkyPNG::Point] A point that describes the top left corner if a
# minimal bounding box would be drawn around all the points in the vector.
def offset
ChunkyPNG::Point.new(min_x, min_y)
end
# Returns the width of the minimal bounding box of all the points in this vector.
# @return [Integer] The x-distance between the points that are farthest from each other.
def width
1 + (max_x - min_x)
end
# Returns the height of the minimal bounding box of all the points in this vector.
# @return [Integer] The y-distance between the points that are farthest from each other.
def height
1 + (max_y - min_y)
end
# Returns the dimension of the minimal bounding rectangle of the points in this vector.
# @return [ChunkyPNG::Dimension] The dimension instance with the width and height
def dimension
ChunkyPNG::Dimension.new(width, height)
end
# @return [Array] The list of points interpreted from the input array.
def self.multiple_from_array(source)
return [] if source.empty?
if source.first.is_a?(Numeric) || source.first =~ /^\d+$/
raise ArgumentError, "The points array is expected to have an even number of items!" if source.length % 2 != 0
points = []
source.each_slice(2) { |x, y| points << ChunkyPNG::Point.new(x, y) }
return points
else
source.map { |p| ChunkyPNG::Point(p) }
end
end
# @return [Array] The list of points parsed from the string.
def self.multiple_from_string(source_str)
multiple_from_array(source_str.scan(/[\(\[\{]?(\d+)\s*[,x]?\s*(\d+)[\)\]\}]?/))
end
end
end
chunky_png-1.3.15/lib/chunky_png/chunk.rb 0000644 0001750 0001750 00000044375 13766004353 017614 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# A PNG datastream consists of multiple chunks. This module, and the classes
# contained within, help with handling these chunks. It supports both reading
# and writing chunks.
#
# All chunk types are instances of the {ChunkyPNG::Chunk::Base} class. For
# some chunk types a specialized class is available, e.g. the IHDR chunk is
# represented by the {ChunkyPNG::Chunk::Header} class. These specialized
# classes help accessing the content of the chunk. All other chunks are
# represented by the {ChunkyPNG::Chunk::Generic} class.
#
# @see ChunkyPNG::Datastream
module Chunk
# Reads a chunk from an IO stream.
#
# @param io [IO, #read] The IO stream to read from.
# @return [ChunkyPNG::Chung::Base] The loaded chunk instance.
def self.read(io)
length, type = read_bytes(io, 8).unpack("Na4")
content = read_bytes(io, length)
crc = read_bytes(io, 4).unpack("N").first
verify_crc!(type, content, crc)
CHUNK_TYPES.fetch(type, Generic).read(type, content)
end
# Reads an exact number of bytes from an IO stream.
# @param io [IO, #read] The IO stream to read from.
# @param length [Integer] The IO exact number of bytes to read.
# @return [String] A binary string of exactly length bytes.
# @raise [ChunkyPNG::ExpectationFailed] If not exactly length
# bytes could be read from the IO stream.
def self.read_bytes(io, length)
data = io.read(length)
raise ExpectationFailed, "Couldn't read #{length} bytes from IO stream." if data.nil? || data.bytesize != length
data
end
# Verifies the CRC of a chunk.
# @param type [String] The chunk's type.
# @param content [String] The chunk's content.
# @param found_crc [Integer] The chunk's found CRC value.
# @raise [ChunkyPNG::CRCMismatch] An exception is raised if
# the found CRC value is not equal to the expected CRC value.
def self.verify_crc!(type, content, found_crc)
expected_crc = Zlib.crc32(content, Zlib.crc32(type))
raise ChunkyPNG::CRCMismatch, "Chuck CRC mismatch!" if found_crc != expected_crc
end
# The base chunk class is the superclass for every chunk type. It contains
# methods to write the chunk to an output stream.
#
# A subclass should implement the +content+ method, which gets called when
# the chunk gets written to a PNG datastream
#
# @abstract
class Base
# The four-character type indicator for the chunk. This field is used to
# find the correct class for a chunk when it is loaded from a PNG stream.
# @return [String]
attr_accessor :type
# Initializes the chunk instance.
# @param type [String] The four character chunk type indicator.
# @param attributes [Hash] A hash of attributes to set on this chunk.
def initialize(type, attributes = {})
self.type = type
attributes.each { |k, v| send("#{k}=", v) }
end
# Writes the chunk to the IO stream, using the provided content.
# The checksum will be calculated and appended to the stream.
# @param io [IO] The IO stream to write to.
# @param content [String] The content for this chunk.
def write_with_crc(io, content)
io << [content.length].pack("N") << type << content
io << [Zlib.crc32(content, Zlib.crc32(type))].pack("N")
end
# Writes the chunk to the IO stream.
#
# It will call the +content+ method to get the content for this chunk,
# and will calculate and append the checksum automatically.
# @param io [IO] The IO stream to write to.
def write(io)
write_with_crc(io, content || "")
end
end
# The Generic chunk type will read the content from the chunk as it,
# and will write it back as it was read.
class Generic < Base
# The attribute to store the content from the chunk, which gets
# written by the +write+ method.
attr_accessor :content
def initialize(type, content = "")
super(type, content: content)
end
# Creates an instance, given the chunk's type and content.
# @param type [String] The four character chunk type indicator.
# @param content [String] The content read from the chunk.
# @return [ChunkyPNG::Chunk::Generic] The new chunk instance.
def self.read(type, content)
new(type, content)
end
end
# The header (IHDR) chunk is the first chunk of every PNG image, and
# contains information about the image: i.e. its width, height, color
# depth, color mode, compression method, filtering method and interlace
# method.
#
# ChunkyPNG supports all values for these variables that are defined in the
# PNG spec, except for color depth: Only 8-bit depth images are supported.
# Note that it is still possible to access the chunk for such an image, but
# ChunkyPNG will raise an exception if you try to access the pixel data.
#
# @see https://www.w3.org/TR/PNG/#11IHDR
class Header < Base
attr_accessor :width, :height, :depth, :color, :compression, :filtering, :interlace
def initialize(attrs = {})
super("IHDR", attrs)
@depth ||= 8
@color ||= ChunkyPNG::COLOR_TRUECOLOR
@compression ||= ChunkyPNG::COMPRESSION_DEFAULT
@filtering ||= ChunkyPNG::FILTERING_DEFAULT
@interlace ||= ChunkyPNG::INTERLACING_NONE
end
# Reads the 13 bytes of content from the header chunk to set the image
# attributes.
# @param type [String] The four character chunk type indicator (= "IHDR").
# @param content [String] The 13 bytes of content read from the chunk.
# @return [ChunkyPNG::Chunk::End] The new Header chunk instance with the
# variables set to the values according to the content.
def self.read(type, content)
fields = content.unpack("NNC5")
new(
width: fields[0],
height: fields[1],
depth: fields[2],
color: fields[3],
compression: fields[4],
filtering: fields[5],
interlace: fields[6]
)
end
# Returns the content for this chunk when it gets written to a file, by
# packing the image information variables into the correct format.
# @return [String] The 13-byte content for the header chunk.
def content
[
width,
height,
depth,
color,
compression,
filtering,
interlace,
].pack("NNC5")
end
end
# The End (IEND) chunk indicates the last chunk of a PNG stream. It does
# not contain any data.
#
# @see https://www.w3.org/TR/PNG/#11IEND
class End < Base
def initialize
super("IEND")
end
# Reads the END chunk. It will check if the content is empty.
# @param type [String] The four character chunk type indicator (=
# "IEND").
# @param content [String] The content read from the chunk. Should be
# empty.
# @return [ChunkyPNG::Chunk::End] The new End chunk instance.
# @raise [ChunkyPNG::ExpectationFailed] Raises an exception if the content was not empty.
def self.read(type, content)
raise ExpectationFailed, "The IEND chunk should be empty!" if content.bytesize > 0
new
end
# Returns an empty string, because this chunk should always be empty.
# @return [""] An empty string.
def content
"".b
end
end
# The Palette (PLTE) chunk contains the image's palette, i.e. the
# 8-bit RGB colors this image is using.
#
# @see https://www.w3.org/TR/PNG/#11PLTE
# @see ChunkyPNG::Chunk::Transparency
# @see ChunkyPNG::Palette
class Palette < Generic
end
# A transparency (tRNS) chunk defines the transparency for an image.
#
# * For indexed images, it contains the alpha channel for the colors
# defined in the Palette (PLTE) chunk.
# * For grayscale images, it contains the grayscale teint that should be
# considered fully transparent.
# * For truecolor images, it contains the color that should be considered
# fully transparent.
#
# Images having a color mode that already includes an alpha channel, this
# chunk should not be included.
#
# @see https://www.w3.org/TR/PNG/#11tRNS
# @see ChunkyPNG::Chunk::Palette
# @see ChunkyPNG::Palette
class Transparency < Generic
# Returns the alpha channel for the palette of an indexed image.
#
# This method should only be used for images having color mode
# ChunkyPNG::COLOR_INDEXED (3).
#
# @return [Array] Returns an array of alpha channel values
# [0-255].
def palette_alpha_channel
content.unpack("C*")
end
# Returns the truecolor entry to be replaced by transparent pixels,
#
# This method should only be used for images having color mode
# ChunkyPNG::COLOR_TRUECOLOR (2).
#
# @return [Integer] The color to replace with fully transparent pixels.
def truecolor_entry(bit_depth)
decode_method_name = :"decode_png_resample_#{bit_depth}bit_value"
values = content.unpack("nnn").map { |c| ChunkyPNG::Canvas.send(decode_method_name, c) }
ChunkyPNG::Color.rgb(*values)
end
# Returns the grayscale entry to be replaced by transparent pixels.
#
# This method should only be used for images having color mode
# ChunkyPNG::COLOR_GRAYSCALE (0).
#
# @return [Integer] The (grayscale) color to replace with fully
# transparent pixels.
def grayscale_entry(bit_depth)
value = ChunkyPNG::Canvas.send(:"decode_png_resample_#{bit_depth}bit_value", content.unpack("n")[0])
ChunkyPNG::Color.grayscale(value)
end
end
# An image data (IDAT) chunk holds (part of) the compressed image pixel data.
#
# The data of an image can be split over multiple chunks, which will have to be combined
# and inflated in order to decode an image. See {{.combine_chunks}} to combine chunks
# to decode, and {{.split_in_chunks}} for encoding a pixeldata stream into IDAT chunks.
#
# @see https://www.w3.org/TR/PNG/#11IDAT
class ImageData < Generic
# Combines the list of IDAT chunks and inflates their contents to produce the
# pixeldata stream for the image.
#
# @return [String] The combined, inflated pixeldata as binary string
def self.combine_chunks(data_chunks)
zstream = Zlib::Inflate.new
data_chunks.each { |c| zstream << c.content }
inflated = zstream.finish
zstream.close
inflated
end
# Splits and compresses a pixeldata stream into a list of IDAT chunks.
#
# @param data [String] The binary string of pixeldata
# @param level [Integer] The compression level to use.
# @param chunk_size [Integer] The maximum size of a chunk.
# @return Array The list of IDAT chunks.
def self.split_in_chunks(data, level = Zlib::DEFAULT_COMPRESSION, chunk_size = 2147483647)
streamdata = Zlib::Deflate.deflate(data, level)
# TODO: Split long streamdata over multiple chunks
[ChunkyPNG::Chunk::ImageData.new("IDAT", streamdata)]
end
end
# The Text (tEXt) chunk contains keyword/value metadata about the PNG
# stream. In this chunk, the value is stored uncompressed.
#
# The tEXt chunk only supports Latin-1 encoded textual data. If you need
# UTF-8 support, check out the InternationalText chunk type.
#
# @see https://www.w3.org/TR/PNG/#11tEXt
# @see ChunkyPNG::Chunk::CompressedText
# @see ChunkyPNG::Chunk::InternationalText
class Text < Base
attr_accessor :keyword, :value
def initialize(keyword, value)
super("tEXt")
@keyword, @value = keyword, value
end
def self.read(type, content)
keyword, value = content.unpack("Z*a*")
new(keyword, value)
end
# Creates the content to write to the stream, by concatenating the
# keyword with the value, joined by a null character.
#
# @return The content that should be written to the datastream.
def content
[keyword, value].pack("Z*a*")
end
end
# The CompressedText (zTXt) chunk contains keyword/value metadata about the
# PNG stream. In this chunk, the value is compressed using Deflate
# compression.
#
# @see https://www.w3.org/TR/PNG/#11zTXt
# @see ChunkyPNG::Chunk::CompressedText
# @see ChunkyPNG::Chunk::InternationalText
class CompressedText < Base
attr_accessor :keyword, :value
def initialize(keyword, value)
super("zTXt")
@keyword, @value = keyword, value
end
def self.read(type, content)
keyword, compression, value = content.unpack("Z*Ca*")
raise ChunkyPNG::NotSupported, "Compression method #{compression.inspect} not supported!" unless compression == ChunkyPNG::COMPRESSION_DEFAULT
new(keyword, Zlib::Inflate.inflate(value))
end
# Creates the content to write to the stream, by concatenating the
# keyword with the deflated value, joined by a null character.
#
# @return The content that should be written to the datastream.
def content
[
keyword,
ChunkyPNG::COMPRESSION_DEFAULT,
Zlib::Deflate.deflate(value),
].pack("Z*Ca*")
end
end
# The Physical (pHYs) chunk specifies the intended pixel size or aspect
# ratio for display of the image.
#
# @see https://www.w3.org/TR/PNG/#11pHYs
class Physical < Base
attr_accessor :ppux, :ppuy, :unit
def initialize(ppux, ppuy, unit = :unknown)
raise ArgumentError, "unit must be either :meters or :unknown" unless [:meters, :unknown].member?(unit)
super("pHYs")
@ppux, @ppuy, @unit = ppux, ppuy, unit
end
def dpix
raise ChunkyPNG::UnitsUnknown, "the PNG specifies its physical aspect ratio, but does not specify the units of its pixels' physical dimensions" unless unit == :meters
ppux * INCHES_PER_METER
end
def dpiy
raise ChunkyPNG::UnitsUnknown, "the PNG specifies its physical aspect ratio, but does not specify the units of its pixels' physical dimensions" unless unit == :meters
ppuy * INCHES_PER_METER
end
def self.read(type, content)
ppux, ppuy, unit = content.unpack("NNC")
unit = unit == 1 ? :meters : :unknown
new(ppux, ppuy, unit)
end
# Assembles the content to write to the stream for this chunk.
# @return [String] The binary content that should be written to the datastream.
def content
[ppux, ppuy, unit == :meters ? 1 : 0].pack("NNC")
end
INCHES_PER_METER = 0.0254
end
# The InternationalText (iTXt) chunk contains keyword/value metadata about the PNG
# stream, translated to a given locale.
#
# The metadata in this chunk can be encoded using UTF-8 characters.
# Moreover, it is possible to define the language of the metadata, and give
# a translation of the keyword name. Finally, it supports bot compressed
# and uncompressed values.
#
# @see https://www.w3.org/TR/PNG/#11iTXt
# @see ChunkyPNG::Chunk::Text
# @see ChunkyPNG::Chunk::CompressedText
class InternationalText < Base
attr_accessor :keyword, :text, :language_tag, :translated_keyword, :compressed, :compression
def initialize(keyword, text, language_tag = "", translated_keyword = "", compressed = ChunkyPNG::UNCOMPRESSED_CONTENT, compression = ChunkyPNG::COMPRESSION_DEFAULT)
super("iTXt")
@keyword = keyword
@text = text
@language_tag = language_tag
@translated_keyword = translated_keyword
@compressed = compressed
@compression = compression
end
# Reads the iTXt chunk.
# @param type [String] The four character chunk type indicator (= "iTXt").
# @param content [String] The content read from the chunk.
# @return [ChunkyPNG::Chunk::InternationalText] The new End chunk instance.
# @raise [ChunkyPNG::InvalidUTF8] If the chunk contains data that is not UTF8-encoded text.
# @raise [ChunkyPNG::NotSupported] If the chunk refers to an unsupported compression method.
# Currently uncompressed data and deflate are supported.
def self.read(type, content)
keyword, compressed, compression, language_tag, translated_keyword, text = content.unpack("Z*CCZ*Z*a*")
raise ChunkyPNG::NotSupported, "Compression flag #{compressed.inspect} not supported!" unless compressed == ChunkyPNG::UNCOMPRESSED_CONTENT || compressed == ChunkyPNG::COMPRESSED_CONTENT
raise ChunkyPNG::NotSupported, "Compression method #{compression.inspect} not supported!" unless compression == ChunkyPNG::COMPRESSION_DEFAULT
text = Zlib::Inflate.inflate(text) if compressed == ChunkyPNG::COMPRESSED_CONTENT
text.force_encoding("utf-8")
raise ChunkyPNG::InvalidUTF8, "Invalid unicode encountered in iTXt chunk" unless text.valid_encoding?
translated_keyword.force_encoding("utf-8")
raise ChunkyPNG::InvalidUTF8, "Invalid unicode encountered in iTXt chunk" unless translated_keyword.valid_encoding?
new(keyword, text, language_tag, translated_keyword, compressed, compression)
end
# Assembles the content to write to the stream for this chunk.
# @return [String] The binary content that should be written to the datastream.
def content
text_field = text.encode("utf-8")
text_field = compressed == ChunkyPNG::COMPRESSED_CONTENT ? Zlib::Deflate.deflate(text_field) : text_field
[keyword, compressed, compression, language_tag, translated_keyword.encode("utf-8"), text_field].pack("Z*CCZ*Z*a*")
end
end
# Maps chunk types to classes, based on the four byte chunk type indicator
# at the beginning of a chunk.
#
# If a chunk type is not specified in this hash, the Generic chunk type
# will be used.
#
# @see ChunkyPNG::Chunk.read
CHUNK_TYPES = {
"IHDR" => Header,
"IEND" => End,
"IDAT" => ImageData,
"PLTE" => Palette,
"tRNS" => Transparency,
"tEXt" => Text,
"zTXt" => CompressedText,
"iTXt" => InternationalText,
"pHYs" => Physical,
}
end
end
chunky_png-1.3.15/lib/chunky_png/canvas.rb 0000644 0001750 0001750 00000032500 13766004353 017742 0 ustar daniel daniel # frozen-string-literal: true
require "chunky_png/canvas/png_encoding"
require "chunky_png/canvas/png_decoding"
require "chunky_png/canvas/adam7_interlacing"
require "chunky_png/canvas/stream_exporting"
require "chunky_png/canvas/stream_importing"
require "chunky_png/canvas/data_url_exporting"
require "chunky_png/canvas/data_url_importing"
require "chunky_png/canvas/operations"
require "chunky_png/canvas/drawing"
require "chunky_png/canvas/resampling"
require "chunky_png/canvas/masking"
module ChunkyPNG
# The ChunkyPNG::Canvas class represents a raster image as a matrix of
# pixels.
#
# This class supports loading a Canvas from a PNG datastream, and creating a
# {ChunkyPNG::Datastream PNG datastream} based on this matrix. ChunkyPNG
# only supports 8-bit color depth, otherwise all of the PNG format's
# variations are supported for both reading and writing.
#
# This class offers per-pixel access to the matrix by using x,y coordinates.
# It uses a palette (see {ChunkyPNG::Palette}) to keep track of the
# different colors used in this matrix.
#
# The pixels in the canvas are stored as 4-byte fixnum, representing 32-bit
# RGBA colors (8 bit per channel). The module {ChunkyPNG::Color} is provided
# to work more easily with these number as color values.
#
# The module {ChunkyPNG::Canvas::Operations} is imported for operations on
# the whole canvas, like cropping and alpha compositing. Simple drawing
# functions are imported from the {ChunkyPNG::Canvas::Drawing} module.
class Canvas
include PNGEncoding
extend PNGDecoding
extend Adam7Interlacing
include StreamExporting
extend StreamImporting
include DataUrlExporting
extend DataUrlImporting
include Operations
include Drawing
include Resampling
include Masking
# @return [Integer] The number of columns in this canvas
attr_reader :width
# @return [Integer] The number of rows in this canvas
attr_reader :height
# @return [Array] The list of pixels in this canvas.
# This array always should have +width * height+ elements.
attr_reader :pixels
#################################################################
# CONSTRUCTORS
#################################################################
# Initializes a new Canvas instance.
#
# @overload initialize(width, height, background_color)
# @param [Integer] width The width in pixels of this canvas
# @param [Integer] height The height in pixels of this canvas
# @param [Integer, ...] background_color The initial background color of
# this canvas. This can be a color value or any value that
# {ChunkyPNG::Color#parse} can handle.
#
# @overload initialize(width, height, initial)
# @param [Integer] width The width in pixels of this canvas
# @param [Integer] height The height in pixels of this canvas
# @param [Array] initial The initial pizel values. Must be an
# array with width * height elements.
def initialize(width, height, initial = ChunkyPNG::Color::TRANSPARENT)
@width, @height = width, height
if initial.is_a?(Array)
pixel_count = width * height
unless initial.length == pixel_count
raise ArgumentError, "The initial array should have #{width}x#{height} = #{pixel_count} elements!"
end
@pixels = initial
else
@pixels = Array.new(width * height, ChunkyPNG::Color.parse(initial))
end
end
# Initializes a new Canvas instances when being cloned.
# @param [ChunkyPNG::Canvas] other The canvas to duplicate
# @return [void]
# @private
def initialize_copy(other)
@width, @height = other.width, other.height
@pixels = other.pixels.dup
end
# Creates a new canvas instance by duplicating another instance.
# @param [ChunkyPNG::Canvas] canvas The canvas to duplicate
# @return [ChunkyPNG::Canvas] The newly constructed canvas instance.
def self.from_canvas(canvas)
new(canvas.width, canvas.height, canvas.pixels.dup)
end
#################################################################
# PROPERTIES
#################################################################
# Returns the dimension (width x height) for this canvas.
# @return [ChunkyPNG::Dimension] A dimension instance with the width and
# height set for this canvas.
def dimension
ChunkyPNG::Dimension.new(width, height)
end
# Returns the area of this canvas in number of pixels.
# @return [Integer] The number of pixels in this canvas
def area
pixels.length
end
# Replaces a single pixel in this canvas.
# @param [Integer] x The x-coordinate of the pixel (column)
# @param [Integer] y The y-coordinate of the pixel (row)
# @param [Integer] color The new color for the provided coordinates.
# @return [Integer] The new color value for this pixel, i.e.
# color.
# @raise [ChunkyPNG::OutOfBounds] when the coordinates are outside of the
# image's dimensions.
# @see #set_pixel
def []=(x, y, color)
assert_xy!(x, y)
@pixels[y * width + x] = ChunkyPNG::Color.parse(color)
end
# Replaces a single pixel in this canvas, without bounds checking.
#
# This method return value and effects are undefined for coordinates
# out of bounds of the canvas.
#
# @param [Integer] x The x-coordinate of the pixel (column)
# @param [Integer] y The y-coordinate of the pixel (row)
# @param [Integer] color The new color for the provided coordinates.
# @return [Integer] The new color value for this pixel, i.e.
# color.
def set_pixel(x, y, color)
@pixels[y * width + x] = color
end
# Replaces a single pixel in this canvas, with bounds checking. It will do
# noting if the provided coordinates are out of bounds.
#
# @param [Integer] x The x-coordinate of the pixel (column)
# @param [Integer] y The y-coordinate of the pixel (row)
# @param [Integer] color The new color value for the provided coordinates.
# @return [Integer] The new color value for this pixel, i.e.
# color, or nil if the coordinates are out of bounds.
def set_pixel_if_within_bounds(x, y, color)
return unless include_xy?(x, y)
@pixels[y * width + x] = color
end
# Returns a single pixel's color value from this canvas.
# @param [Integer] x The x-coordinate of the pixel (column)
# @param [Integer] y The y-coordinate of the pixel (row)
# @return [Integer] The current color value at the provided coordinates.
# @raise [ChunkyPNG::OutOfBounds] when the coordinates are outside of the
# image's dimensions.
# @see #get_pixel
def [](x, y)
assert_xy!(x, y)
@pixels[y * width + x]
end
# Returns a single pixel from this canvas, without checking bounds. The
# return value for this method is undefined if the coordinates are out of
# bounds.
#
# @param (see #[])
# @return [Integer] The current pixel at the provided coordinates.
def get_pixel(x, y)
@pixels[y * width + x]
end
# Returns an extracted row as vector of pixels
# @param [Integer] y The 0-based row index
# @return [Array] The vector of pixels in the requested row
def row(y)
assert_y!(y)
pixels.slice(y * width, width)
end
# Returns an extracted column as vector of pixels.
# @param [Integer] x The 0-based column index.
# @return [Array] The vector of pixels in the requested column.
def column(x)
assert_x!(x)
(0...height).inject([]) { |pixels, y| pixels << get_pixel(x, y) }
end
# Replaces a row of pixels on this canvas.
# @param [Integer] y The 0-based row index.
# @param [Array] vector The vector of pixels to replace the row
# with.
# @return [void]
def replace_row!(y, vector)
assert_y!(y) && assert_width!(vector.length)
pixels[y * width, width] = vector
end
# Replaces a column of pixels on this canvas.
# @param [Integer] x The 0-based column index.
# @param [Array] vector The vector of pixels to replace the column
# with.
# @return [void]
def replace_column!(x, vector)
assert_x!(x) && assert_height!(vector.length)
for y in 0...height do
set_pixel(x, y, vector[y])
end
end
# Checks whether the given coordinates are in the range of the canvas
# @param [ChunkyPNG::Point, Array, Hash, String] point_like The point to
# check.
# @return [true, false] True if the x and y coordinates of the point are
# within the limits of this canvas.
# @see ChunkyPNG.Point
def include_point?(*point_like)
dimension.include?(ChunkyPNG::Point(*point_like))
end
alias include? include_point?
# Checks whether the given x- and y-coordinate are in the range of the
# canvas
#
# @param [Integer] x The x-coordinate of the pixel (column)
# @param [Integer] y The y-coordinate of the pixel (row)
# @return [true, false] True if the x- and y-coordinate is in the range of
# this canvas.
def include_xy?(x, y)
y >= 0 && y < height && x >= 0 && x < width
end
# Checks whether the given y-coordinate is in the range of the canvas
# @param [Integer] y The y-coordinate of the pixel (row)
# @return [true, false] True if the y-coordinate is in the range of this
# canvas.
def include_y?(y)
y >= 0 && y < height
end
# Checks whether the given x-coordinate is in the range of the canvas
# @param [Integer] x The y-coordinate of the pixel (column)
# @return [true, false] True if the x-coordinate is in the range of this
# canvas.
def include_x?(x)
x >= 0 && x < width
end
# Returns the palette used for this canvas.
# @return [ChunkyPNG::Palette] A palette which contains all the colors that
# are being used for this image.
def palette
ChunkyPNG::Palette.from_canvas(self)
end
# Equality check to compare this canvas with other matrices.
# @param other The object to compare this Matrix to.
# @return [true, false] True if the size and pixel values of the other
# canvas are exactly the same as this canvas's size and pixel values.
def eql?(other)
other.is_a?(self.class) &&
other.pixels == pixels &&
other.width == width &&
other.height == height
end
alias == eql?
#################################################################
# EXPORTING
#################################################################
# Creates an ChunkyPNG::Image object from this canvas.
# @return [ChunkyPNG::Image] This canvas wrapped in an Image instance.
def to_image
ChunkyPNG::Image.from_canvas(self)
end
# Alternative implementation of the inspect method.
# @return [String] A nicely formatted string representation of this canvas.
# @private
def inspect
inspected = +"<#{self.class.name} #{width}x#{height} ["
for y in 0...height
inspected << "\n\t[" << row(y).map { |p| ChunkyPNG::Color.to_hex(p) }.join(" ") << "]"
end
inspected << "\n]>"
end
protected
# Replaces the image, given a new width, new height, and a new pixel array.
def replace_canvas!(new_width, new_height, new_pixels)
unless new_pixels.length == new_width * new_height
raise ArgumentError, "The provided pixel array should have #{new_width * new_height} items"
end
@width, @height, @pixels = new_width, new_height, new_pixels
self
end
# Throws an exception if the x-coordinate is out of bounds.
def assert_x!(x)
unless include_x?(x)
raise ChunkyPNG::OutOfBounds, "Column index #{x} out of bounds!"
end
true
end
# Throws an exception if the y-coordinate is out of bounds.
def assert_y!(y)
unless include_y?(y)
raise ChunkyPNG::OutOfBounds, "Row index #{y} out of bounds!"
end
true
end
# Throws an exception if the x- or y-coordinate is out of bounds.
def assert_xy!(x, y)
unless include_xy?(x, y)
raise ChunkyPNG::OutOfBounds, "Coordinates (#{x},#{y}) out of bounds!"
end
true
end
# Throws an exception if the vector_length does not match this canvas'
# height.
def assert_height!(vector_length)
if height != vector_length
raise ChunkyPNG::ExpectationFailed,
"The length of the vector (#{vector_length}) does not match the canvas height (#{height})!"
end
true
end
# Throws an exception if the vector_length does not match this canvas'
# width.
def assert_width!(vector_length)
if width != vector_length
raise ChunkyPNG::ExpectationFailed,
"The length of the vector (#{vector_length}) does not match the canvas width (#{width})!"
end
true
end
# Throws an exception if the matrix width and height does not match this canvas' dimensions.
def assert_size!(matrix_width, matrix_height)
if width != matrix_width
raise ChunkyPNG::ExpectationFailed,
"The width of the matrix does not match the canvas width!"
end
if height != matrix_height
raise ChunkyPNG::ExpectationFailed,
"The height of the matrix does not match the canvas height!"
end
true
end
end
end
chunky_png-1.3.15/lib/chunky_png/point.rb 0000644 0001750 0001750 00000010757 13766004353 017632 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# Factory method to create {ChunkyPNG::Point} instances.
#
# This method tries to be as flexible as possible with regards to the given input: besides
# explicit coordinates, this method also accepts arrays, hashes, strings, {ChunkyPNG::Dimension}
# instances and anything that responds to :x and :y.
#
# @overload Point(x, y)
# @param [Integer, :to_i] x The x-coordinate
# @param [Integer, :to_i] y The y-coordinate
# @return [ChunkyPNG::Point] The instantiated point.
#
# @overload Point(array)
# @param [Array] array A two element array which represent the x- and y-coordinate.
# @return [ChunkyPNG::Point] The instantiated point.
#
# @overload Point(hash)
# @param [Hash] array A hash with the :x or 'x' and :y or
# 'y' keys set, which will be used as coordinates.
# @return [ChunkyPNG::Point] The instantiated point.
#
# @overload Point(string)
# @param [String] string A string that contains the coordinates, e.g. '0, 4',
# '(0 4)', [0,4}', etc.
# @return [ChunkyPNG::Point] The instantiated point.
#
# @return [ChunkyPNG::Point]
# @raise [ArgumentError] if the arguments weren't understood.
# @see ChunkyPNG::Point
def self.Point(*args)
case args.length
when 2 then ChunkyPNG::Point.new(*args)
when 1 then build_point_from_object(args.first)
else raise ArgumentError,
"Don't know how to construct a point from #{args.inspect}!"
end
end
def self.build_point_from_object(source)
case source
when ChunkyPNG::Point
source
when ChunkyPNG::Dimension
ChunkyPNG::Point.new(source.width, source.height)
when Array
ChunkyPNG::Point.new(source[0], source[1])
when Hash
x = source[:x] || source["x"]
y = source[:y] || source["y"]
ChunkyPNG::Point.new(x, y)
when ChunkyPNG::Point::POINT_REGEXP
ChunkyPNG::Point.new($1.to_i, $2.to_i)
else
if source.respond_to?(:x) && source.respond_to?(:y)
ChunkyPNG::Point.new(source.x, source.y)
else
raise ArgumentError,
"Don't know how to construct a point from #{source.inspect}!"
end
end
end
private_class_method :build_point_from_object
# Simple class that represents a point on a canvas using an x and y coordinate.
#
# This class implements some basic methods to handle comparison, the splat operator and
# bounds checking that make it easier to work with coordinates.
#
# @see ChunkyPNG.Point
class Point
# @return [Regexp] The regexp to parse points from a string.
# @private
POINT_REGEXP = /^[\(\[\{]?(\d+)\s*[,]?\s*(\d+)[\)\]\}]?$/
# @return [Integer] The x-coordinate of the point.
attr_accessor :x
# @return [Integer] The y-coordinate of the point.
attr_accessor :y
# Initializes a new point instance.
# @param [Integer, :to_i] x The x-coordinate.
# @param [Integer, :to_i] y The y-coordinate.
def initialize(x, y)
@x, @y = x.to_i, y.to_i
end
# Checks whether 2 points are identical.
# @return [true, false] true iff the x and y coordinates match
def eql?(other)
other.x == x && other.y == y
end
alias == eql?
# Compares 2 points.
#
# It will first compare the y coordinate, and it only takes the x-coordinate into
# account if the y-coordinates of the points are identical. This way, an array of
# points will be sorted into the order in which they would occur in the pixels
# array returned by {ChunkyPNG::Canvas#pixels}.
#
# @param [ChunkyPNG::Point] other The point to compare this point with.
# @return [-1, 0, 1] -1 If this point comes before the other one, 1
# if after, and 0 if the points are identical.
def <=>(other)
(y <=> other.y) == 0 ? x <=> other.x : y <=> other.y
end
# Converts the point instance to an array.
# @return [Array] A 2-element array, i.e. [x, y].
def to_a
[x, y]
end
alias to_ary to_a
# Checks whether the point falls into a dimension
# @param [ChunkyPNG::Dimension, ...] dimension_like The dimension of which the bounds
# should be taken for the check.
# @return [true, false] true iff the x and y coordinate fall width the width
# and height of the dimension.
def within_bounds?(*dimension_like)
ChunkyPNG::Dimension(*dimension_like).include?(self)
end
end
end
chunky_png-1.3.15/lib/chunky_png/rmagick.rb 0000644 0001750 0001750 00000002604 13766004353 020106 0 ustar daniel daniel # frozen-string-literal: true
require "rmagick"
module ChunkyPNG
# Methods for importing and exporting RMagick image objects.
#
# By default, this module is disabled because of the dependency on RMagick.
# You need to include this module yourself if you want to use it.
#
# @example
#
# require 'rmagick'
# require 'chunky_png/rmagick'
#
# canvas = ChunkyPNG::Canvas.from_file('filename.png')
# image = ChunkyPNG::RMagick.export(canvas)
#
# # do something with the image using RMagick
#
# updated_canvas = ChunkyPNG::RMagick.import(image)
#
module RMagick
extend self
# Imports an RMagick image as Canvas object.
# @param [Magick::Image] image The image to import
# @return [ChunkyPNG::Canvas] The canvas, constructed from the RMagick image.
def import(image)
pixels = image.export_pixels_to_str(0, 0, image.columns, image.rows, "RGBA")
ChunkyPNG::Canvas.from_rgba_stream(image.columns, image.rows, pixels)
end
# Exports a Canvas as RMagick image instance.
# @param [ChunkyPNG::Canvas] canvas The canvas to export.
# @return [Magick::Image] The RMagick image constructed from the Canvas instance.
def export(canvas)
image = Magick::Image.new(canvas.width, canvas.height)
image.import_pixels(0, 0, canvas.width, canvas.height, "RGBA", canvas.pixels.pack("N*"))
image
end
end
end
chunky_png-1.3.15/lib/chunky_png/version.rb 0000644 0001750 0001750 00000000262 13766004353 020154 0 ustar daniel daniel # frozen-string-literal: true
module ChunkyPNG
# The current version of ChunkyPNG.
# Set it and commit the change this before running rake release.
VERSION = "1.3.15"
end
chunky_png-1.3.15/lib/chunky_png.rb 0000644 0001750 0001750 00000011725 13766004353 016475 0 ustar daniel daniel # frozen-string-literal: true
# Basic requirements from standard library
require "set"
require "zlib"
require "stringio"
# ChunkyPNG - the pure ruby library to access PNG files.
#
# The ChunkyPNG module defines some constants that are used in the
# PNG specification, specifies some exception classes, and serves as
# a namespace for all the other modules and classes in this library.
#
# {ChunkyPNG::Image}:: class to represent PNG images, including metadata.
# {ChunkyPNG::Canvas}:: class to represent the image's canvas.
# {ChunkyPNG::Color}:: module to work with color values.
# {ChunkyPNG::Palette}:: represents the palette of colors used on a {ChunkyPNG::Canvas}.
# {ChunkyPNG::Datastream}:: represents the internal structure of a PNG {ChunkyPNG::Image}.
# {ChunkyPNG::Color}:: represents one chunk of data within a {ChunkyPNG::Datastream}.
# {ChunkyPNG::Point}:: geometry helper class representing a 2-dimensional point.
# {ChunkyPNG::Dimension}:: geometry helper class representing a dimension (i.e. width x height).
# {ChunkyPNG::Vector}:: geometry helper class representing a series of points.
#
# @author Willem van Bergen
module ChunkyPNG
###################################################
# PNG international standard defined constants
###################################################
# Indicates that the PNG image uses grayscale colors, i.e. only a
# single teint channel.
# @private
COLOR_GRAYSCALE = 0
# Indicates that the PNG image uses true color, composed of a red
# green and blue channel.
# @private
COLOR_TRUECOLOR = 2
# Indicates that the PNG image uses indexed colors, where the values
# point to colors defined on a palette.
# @private
COLOR_INDEXED = 3
# Indicates that the PNG image uses grayscale colors with opacity, i.e.
# a teint channel with an alpha channel.
# @private
COLOR_GRAYSCALE_ALPHA = 4
# Indicates that the PNG image uses true color with opacity, composed of
# a red, green and blue channel, and an alpha value.
# @private
COLOR_TRUECOLOR_ALPHA = 6
# Indicates that the PNG specification's default compression
# method is used (Zlib/Deflate)
# @private
COMPRESSION_DEFAULT = 0
# Indicates that the PNG chunk content is not compressed
# flag used in iTXt chunk
# @private
UNCOMPRESSED_CONTENT = 0
# Indicates that the PNG chunk content is compressed
# flag used in iTXt chunk
# @private
COMPRESSED_CONTENT = 1
# Indicates that the image does not use interlacing.
# @private
INTERLACING_NONE = 0
# Indicates that the image uses Adam7 interlacing.
# @private
INTERLACING_ADAM7 = 1
### Filter method constants
# Indicates that the PNG specification's default filtering are
# being used in the image.
# @private
FILTERING_DEFAULT = 0
# Indicates that no filtering is used for the scanline.
# @private
FILTER_NONE = 0
# Indicates that SUB filtering is used for the scanline.
# @private
FILTER_SUB = 1
# Indicates that UP filtering is used for the scanline.
# @private
FILTER_UP = 2
# Indicates that AVERAGE filtering is used for the scanline.
# @private
FILTER_AVERAGE = 3
# Indicates that PAETH filtering is used for the scanline.
# @private
FILTER_PAETH = 4
###################################################
# ChunkyPNG exception classes
###################################################
# Default exception class for ChunkyPNG
class Exception < ::StandardError
end
# Exception that is raised for an unsupported PNG image.
class NotSupported < ChunkyPNG::Exception
end
# Exception that is raised if the PNG signature is not encountered at the
# beginning of the file.
class SignatureMismatch < ChunkyPNG::Exception
end
# Exception that is raised if the CRC check for a block fails
class CRCMismatch < ChunkyPNG::Exception
end
# Exception that is raised if an tTXt chunk does not contain valid UTF-8 data.
class InvalidUTF8 < ChunkyPNG::Exception
end
# Exception that is raised if an expectation fails.
class ExpectationFailed < ChunkyPNG::Exception
end
# Exception that when provided coordinates are out of bounds for the canvas
class OutOfBounds < ChunkyPNG::ExpectationFailed
end
# Exception that is raised when requesting the DPI of a PNG that doesn't
# specify the units of its physical pixel dimensions.
class UnitsUnknown < ChunkyPNG::Exception
end
# Null-byte, with the encoding set correctly to ASCII-8BIT (binary) in Ruby 1.9.
# @return [String] A binary string, consisting of one NULL-byte.
# @private
EXTRA_BYTE = "\0".b
end
require "chunky_png/version"
# PNG file structure
require "chunky_png/datastream"
require "chunky_png/chunk"
# Colors
require "chunky_png/palette"
require "chunky_png/color"
# Geometry
require "chunky_png/point"
require "chunky_png/vector"
require "chunky_png/dimension"
# Canvas / Image classes
require "chunky_png/canvas"
require "chunky_png/image"
chunky_png-1.3.15/README.md 0000644 0001750 0001750 00000007437 13766004353 014521 0 ustar daniel daniel # ChunkyPNG
This library can read and write PNG files. It is written in pure Ruby for
maximum portability. Let me rephrase: it does NOT require RMagick or any other
memory leaking image library.
- [Source code](https://github.com/wvanbergen/chunky_png/tree/master)
- [RDoc](https://rdoc.info/gems/chunky_png)
- [Wiki](https://github.com/wvanbergen/chunky_png/wiki)
- [Issue tracker](https://github.com/wvanbergen/chunky_png/issues)
## Features
- Decodes any image that the PNG standard allows. This includes all standard
color modes, all bit depths, all transparency, and interlacing and filtering
options.
- Encodes images supports all color modes (true color, grayscale, and indexed)
and transparency for all these color modes. The best color mode will be
chosen automatically, based on the amount of used colors.
- R/W access to the image's pixels.
- R/W access to all image metadata that is stored in chunks.
- Memory efficient (uses a Fixnum, i.e. 4 or 8 bytes of memory per pixel,
depending on the hardware)
- Reasonably fast for Ruby standards, by only using integer math and a highly
optimized saving routine.
- Works on every currently supported Ruby version (2.5+)
- Interoperability with RMagick if you really have to.
Also, have a look at [OilyPNG](https://github.com/wvanbergen/oily_png) which
is a mixin module that implements some of the ChunkyPNG algorithms in C, which
provides a massive speed boost to encoding and decoding.
## Usage
```ruby
require 'chunky_png'
# Creating an image from scratch, save as an interlaced PNG
png = ChunkyPNG::Image.new(16, 16, ChunkyPNG::Color::TRANSPARENT)
png[1,1] = ChunkyPNG::Color.rgba(10, 20, 30, 128)
png[2,1] = ChunkyPNG::Color('black @ 0.5')
png.save('filename.png', :interlace => true)
# Compose images using alpha blending.
avatar = ChunkyPNG::Image.from_file('avatar.png')
badge = ChunkyPNG::Image.from_file('no_ie_badge.png')
avatar.compose!(badge, 10, 10)
avatar.save('composited.png', :fast_rgba) # Force the fast saving routine.
# Accessing metadata
image = ChunkyPNG::Image.from_file('with_metadata.png')
puts image.metadata['Title']
image.metadata['Author'] = 'Willem van Bergen'
image.save('with_metadata.png') # Overwrite file
# Low level access to PNG chunks
png_stream = ChunkyPNG::Datastream.from_file('filename.png')
png_stream.each_chunk { |chunk| p chunk.type }
```
Also check out the screencast on the ChunkyPNG homepage by John Davison,
which illustrates basic usage of the library on the [ChunkyPNG
website](https://chunkypng.com/).
For more information, see the [project
wiki](https://github.com/wvanbergen/chunky_png/wiki) or the [RDOC
documentation](https://www.rubydoc.info/gems/chunky_png).
## Security warning
ChunkyPNG is vulnerable to decompression bombs, which means that ChunkyPNG is
vulnerable to DOS attacks by running out of memory when loading a specifically
crafted PNG file. Because of the pure-Ruby nature of the library it is very hard
to fix this problem in the library itself.
In order to safely deal with untrusted images, you should make sure to do the
image processing using ChunkyPNG in a separate process, e.g. by using fork or a
background processing library.
## About
The library is written by Willem van Bergen for Floorplanner.com, and released
under the MIT license (see LICENSE). Please contact me for questions or
remarks.
I generally consider this library to be feature complete. I will gladly accept
patches to fix bugs and improve performance, but I will generally be hesitant
to accept new features or API endpoints. Before contributing, please read
[CONTRIBUTING.rdoc](CONTRIBUTING.rdoc) that explains this in more detail.
Please check out CHANGELOG.rdoc to see what changed in all versions.
P.S.: The name of this library is intentionally similar to Chunky Bacon and
Chunky GIF. Use Google if you want to know _why_. :-)
chunky_png-1.3.15/.standard.yml 0000644 0001750 0001750 00000001157 13766004353 015634 0 ustar daniel daniel # ChunkyPNG uses and enforces standard.rb as code style (see https://github.com/testdouble/standard).
# For backwards compatilibity and idiosyncratic preferences of the main author,
# there are some minor differences listed in here.
ruby_version: 2.2
ignore:
- lib/chunky_png/**/*.rb:
# We allow `for` loops in the codebase, especially in hot paths,
# because they perform better than `each` blocks.
- "Style/For"
- spec/chunky_png/**/*.rb:
# In RSpec, having to follow this rule will cause expectations to
# be less readable, specifically blocks for the `change` matcher.
- "Lint/AmbiguousBlockAssociation"
chunky_png-1.3.15/tasks/ 0000755 0001750 0001750 00000000000 13766004353 014354 5 ustar daniel daniel chunky_png-1.3.15/tasks/benchmarks.rake 0000644 0001750 0001750 00000001241 13766004353 017333 0 ustar daniel daniel all_benchamrk_tasks = []
namespace(:benchmark) do
Dir[File.join(File.dirname(__FILE__), "..", "benchmarks", "*_benchmark.rb")]. each do |benchmark_file|
task_name = File.basename(benchmark_file, "_benchmark.rb").to_sym
desc "Run the #{task_name} benchmark."
task(task_name, :n) do |task, args|
ENV["N"] = args[:n] if args[:n]
load(File.expand_path(benchmark_file))
end
all_benchamrk_tasks << "benchmark:#{task_name}"
end
end
unless all_benchamrk_tasks.empty?
desc "Run the whole benchmark suite"
task(:benchmark, :n) do |task, args|
all_benchamrk_tasks.each do |t|
task(t).invoke(args[:n])
puts
end
end
end
chunky_png-1.3.15/Gemfile 0000644 0001750 0001750 00000000422 13766004353 014520 0 ustar daniel daniel # frozen-string-literal: true
source "https://rubygems.org"
gemspec
platforms :jruby do
gem "jruby-openssl"
end
group :jekyll do
gem "jekyll", "~> 3.3"
gem "kramdown-parser-gfm"
end
group :jekyll_plugins do
gem "jekyll-commonmark"
gem "jekyll-theme-cayman"
end chunky_png-1.3.15/spec/ 0000755 0001750 0001750 00000000000 13766004353 014161 5 ustar daniel daniel chunky_png-1.3.15/spec/chunky_png/ 0000755 0001750 0001750 00000000000 13766004353 016326 5 ustar daniel daniel chunky_png-1.3.15/spec/chunky_png/datastream_spec.rb 0000644 0001750 0001750 00000014343 13766004353 022017 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Datastream do
describe ".from_io" do
it "should raise an error when loading a file with a bad signature" do
filename = resource_file("damaged_signature.png")
expect { ChunkyPNG::Datastream.from_file(filename) }.to raise_error(ChunkyPNG::SignatureMismatch)
end
it "should raise an error if the CRC of a chunk is incorrect" do
filename = resource_file("damaged_chunk.png")
expect { ChunkyPNG::Datastream.from_file(filename) }.to raise_error(ChunkyPNG::CRCMismatch)
end
it "should raise an error for a stream that is too short" do
stream = StringIO.new
expect { ChunkyPNG::Datastream.from_io(stream) }.to raise_error(ChunkyPNG::SignatureMismatch)
end
it "should read a stream with trailing data without failing" do
filename = resource_file("trailing_bytes_after_iend_chunk.png")
image = ChunkyPNG::Datastream.from_file(filename)
expect(image).to be_instance_of(ChunkyPNG::Datastream)
end
end
describe "#metadata" do
it "should load uncompressed tXTt chunks correctly" do
filename = resource_file("text_chunk.png")
ds = ChunkyPNG::Datastream.from_file(filename)
expect(ds.metadata["Title"]).to eql "My amazing icon!"
expect(ds.metadata["Author"]).to eql "Willem van Bergen"
end
it "should load compressed zTXt chunks correctly" do
filename = resource_file("ztxt_chunk.png")
ds = ChunkyPNG::Datastream.from_file(filename)
expect(ds.metadata["Title"]).to eql "PngSuite"
expect(ds.metadata["Copyright"]).to eql "Copyright Willem van Schaik, Singapore 1995-96"
end
it "ignores iTXt chunks" do
filename = resource_file("itxt_chunk.png")
ds = ChunkyPNG::Datastream.from_file(filename)
expect(ds.metadata).to be_empty
end
end
describe "#physical_chunk" do
it "should read and write pHYs chunks correctly" do
filename = resource_file("clock.png")
ds = ChunkyPNG::Datastream.from_file(filename)
expect(ds.physical_chunk.unit).to eql :meters
expect(ds.physical_chunk.dpix.round).to eql 72
expect(ds.physical_chunk.dpiy.round).to eql 72
ds = ChunkyPNG::Datastream.from_blob(ds.to_blob)
expect(ds.physical_chunk).not_to be_nil
end
it "should raise ChunkyPNG::UnitsUnknown if we request dpi but the units are unknown" do
physical_chunk = ChunkyPNG::Chunk::Physical.new(2835, 2835, :unknown)
expect { physical_chunk.dpix }.to raise_error(ChunkyPNG::UnitsUnknown)
expect { physical_chunk.dpiy }.to raise_error(ChunkyPNG::UnitsUnknown)
end
end
describe "#iTXt_chunk" do
it "should read iTXt chunks correctly" do
filename = resource_file("itxt_chunk.png")
ds = ChunkyPNG::Datastream.from_file(filename)
int_text_chunks = ds.chunks.select { |chunk| chunk.is_a?(ChunkyPNG::Chunk::InternationalText) }
expect(int_text_chunks.length).to eq(2)
coach_uk = int_text_chunks.find { |chunk| chunk.language_tag == "en-gb" }
coach_us = int_text_chunks.find { |chunk| chunk.language_tag == "en-us" }
expect(coach_uk).to_not be_nil
expect(coach_us).to_not be_nil
expect(coach_uk.keyword).to eq("coach")
expect(coach_uk.text).to eq("bus with of higher standard of comfort, usually chartered or used for longer journeys")
expect(coach_uk.translated_keyword).to eq("bus")
expect(coach_uk.compressed).to eq(ChunkyPNG::UNCOMPRESSED_CONTENT)
expect(coach_uk.compression).to eq(ChunkyPNG::COMPRESSION_DEFAULT)
expect(coach_us.keyword).to eq("coach")
expect(coach_us.text).to eq("US extracurricular sports teacher at a school (UK: PE teacher) lowest class on a passenger aircraft (UK: economy)")
expect(coach_us.translated_keyword).to eq("trainer")
expect(coach_us.compressed).to eq(ChunkyPNG::COMPRESSED_CONTENT)
expect(coach_us.compression).to eq(ChunkyPNG::COMPRESSION_DEFAULT)
end
it "should write iTXt chunks correctly" do
expected_hex = %w[0000 001d 6954 5874 436f 6d6d 656e 7400 0000 0000 4372 6561 7465 6420 7769 7468 2047 494d 5064 2e65 07].join("")
stream = StringIO.new
itext = ChunkyPNG::Chunk::InternationalText.new("Comment", "Created with GIMP")
itext.write(stream)
generated_hex = stream.string.unpack("H*").join("")
expect(generated_hex).to eq(expected_hex)
end
it "should handle incorrect UTF-8 encoding in iTXt chunks" do
incorrect_text_encoding = [0, 0, 0, 14, 105, 84, 88, 116, 67, 111, 109, 109, 101, 110, 116, 0, 0, 0, 0, 0, 195, 40, 17, 87, 97, 213].pack("C*")
incorrect_translated_keyword_encoding = [0, 0, 0, 19, 105, 84, 88, 116, 67, 111, 109, 109, 101, 110, 116, 0, 0, 0, 0, 226, 130, 40, 0, 116, 101, 115, 116, 228, 53, 113, 182].pack("C*")
expect { ChunkyPNG::Chunk.read(StringIO.new(incorrect_text_encoding)) }.to raise_error(ChunkyPNG::InvalidUTF8)
expect { ChunkyPNG::Chunk.read(StringIO.new(incorrect_translated_keyword_encoding)) }.to raise_error(ChunkyPNG::InvalidUTF8)
end
it "should handle UTF-8 in iTXt compressed chunks correctly" do
parsed = serialized_chunk(ChunkyPNG::Chunk::InternationalText.new("Comment", "✨", "", "💩", ChunkyPNG::COMPRESSED_CONTENT))
expect(parsed.text).to eq("✨")
expect(parsed.text.encoding).to eq(Encoding::UTF_8)
expect(parsed.translated_keyword).to eq("💩")
expect(parsed.translated_keyword.encoding).to eq(Encoding::UTF_8)
end
it "should handle UTF-8 in iTXt chunks correctly" do
parsed = serialized_chunk(ChunkyPNG::Chunk::InternationalText.new("Comment", "✨", "", "💩"))
expect(parsed.text).to eq("✨")
expect(parsed.text.encoding).to eq(Encoding::UTF_8)
expect(parsed.translated_keyword).to eq("💩")
expect(parsed.translated_keyword.encoding).to eq(Encoding::UTF_8)
end
it "should transform non UTF-8 iTXt fields to UTF-8 on write" do
parsed = serialized_chunk(ChunkyPNG::Chunk::InternationalText.new("Comment", "®".encode("Windows-1252"), "", "ƒ".encode("Windows-1252")))
expect(parsed.text).to eq("®")
expect(parsed.text.encoding).to eq(Encoding::UTF_8)
expect(parsed.translated_keyword).to eq("ƒ")
expect(parsed.translated_keyword.encoding).to eq(Encoding::UTF_8)
end
end
end
chunky_png-1.3.15/spec/chunky_png/color_spec.rb 0000644 0001750 0001750 00000035247 13766004353 021016 0 ustar daniel daniel require "spec_helper"
describe "ChunyPNG.Color" do
it "should interpret 4 arguments as RGBA values" do
expect(ChunkyPNG::Color(1, 2, 3, 4)).to eql ChunkyPNG::Color.rgba(1, 2, 3, 4)
end
it "should interpret 3 arguments as RGBA values" do
expect(ChunkyPNG::Color(1, 2, 3)).to eql ChunkyPNG::Color.rgb(1, 2, 3)
end
it "should interpret 2 arguments as a color to parse and an opacity value" do
expect(ChunkyPNG::Color("0x0a649664", 0xaa)).to eql 0x0a6496aa
expect(ChunkyPNG::Color("spring green @ 0.6666", 0xff)).to eql 0x00ff7fff
end
it "should interpret 1 argument as a color to parse" do
expect(ChunkyPNG::Color).to receive(:parse).with("0x0a649664")
ChunkyPNG::Color("0x0a649664")
end
end
describe ChunkyPNG::Color do
include ChunkyPNG::Color
before(:each) do
@white = 0xffffffff
@black = 0x000000ff
@opaque = 0x0a6496ff
@non_opaque = 0x0a649664
@fully_transparent = 0x0a649600
@red = 0xff0000ff
@green = 0x00ff00ff
@blue = 0x0000ffff
end
describe "#parse" do
it "should interpret a hex string correctly" do
expect(parse("0x0a649664")).to eql ChunkyPNG::Color.from_hex("#0a649664")
end
it "should interpret a color name correctly" do
expect(parse(:spring_green)).to eql 0x00ff7fff
expect(parse("spring green")).to eql 0x00ff7fff
expect(parse("spring green @ 0.6666")).to eql 0x00ff7faa
end
it "should return numbers as is" do
expect(parse("12345")).to eql 12345
expect(parse(12345)).to eql 12345
end
end
describe "#pixel_bytesize" do
it "should return the normal amount of bytes with a bit depth of 8" do
expect(pixel_bytesize(ChunkyPNG::COLOR_TRUECOLOR, 8)).to eql 3
end
it "should return a multiple of the normal amount of bytes with a bit depth greater than 8" do
expect(pixel_bytesize(ChunkyPNG::COLOR_TRUECOLOR, 16)).to eql 6
expect(pixel_bytesize(ChunkyPNG::COLOR_TRUECOLOR_ALPHA, 16)).to eql 8
expect(pixel_bytesize(ChunkyPNG::COLOR_GRAYSCALE_ALPHA, 16)).to eql 4
end
it "should return 1 with a bit depth lower than 0" do
expect(pixel_bytesize(ChunkyPNG::COLOR_TRUECOLOR, 4)).to eql 1
expect(pixel_bytesize(ChunkyPNG::COLOR_INDEXED, 2)).to eql 1
expect(pixel_bytesize(ChunkyPNG::COLOR_GRAYSCALE_ALPHA, 1)).to eql 1
end
end
describe "#pass_bytesize" do
it "should calculate a pass size correctly" do
expect(pass_bytesize(ChunkyPNG::COLOR_TRUECOLOR, 8, 10, 10)).to eql 310
end
it "should return 0 if one of the dimensions is zero" do
expect(pass_bytesize(ChunkyPNG::COLOR_TRUECOLOR, 8, 0, 10)).to eql 0
expect(pass_bytesize(ChunkyPNG::COLOR_TRUECOLOR, 8, 10, 0)).to eql 0
end
end
describe "#rgba" do
it "should represent pixels as the correct number" do
# rubocop:disable Layout/ExtraSpacing, Layout/SpaceInsideParens
expect(rgba(255, 255, 255, 255)).to eql @white
expect(rgba( 0, 0, 0, 255)).to eql @black
expect(rgba( 10, 100, 150, 255)).to eql @opaque
expect(rgba( 10, 100, 150, 100)).to eql @non_opaque
expect(rgba( 10, 100, 150, 0)).to eql @fully_transparent
# rubocop:enable Layout/ExtraSpacing, Layout/SpaceInsideParens
end
end
describe "#from_hex" do
it "should load colors correctly from hex notation" do
expect(from_hex("0a649664")).to eql @non_opaque
expect(from_hex("#0a649664")).to eql @non_opaque
expect(from_hex("0x0a649664")).to eql @non_opaque
expect(from_hex("0a6496")).to eql @opaque
expect(from_hex("#0a6496")).to eql @opaque
expect(from_hex("0x0a6496")).to eql @opaque
expect(from_hex("abc")).to eql 0xaabbccff
expect(from_hex("#abc")).to eql 0xaabbccff
expect(from_hex("0xabc")).to eql 0xaabbccff
end
it "should allow setting opacity explicitly" do
expect(from_hex("0x0a6496", 0x64)).to eql @non_opaque
expect(from_hex("#0a6496", 0x64)).to eql @non_opaque
expect(from_hex("0xabc", 0xdd)).to eql 0xaabbccdd
expect(from_hex("#abc", 0xdd)).to eql 0xaabbccdd
end
end
describe "#from_hsv" do
it "should load colors correctly from an HSV triple" do
# At 0 brightness, should be @black independent of hue or sat
expect(from_hsv(0, 0, 0)).to eql @black
expect(from_hsv(100, 1, 0)).to eql @black
expect(from_hsv(100, 0.5, 0)).to eql @black
# At brightness 1 and sat 0, should be @white regardless of hue
expect(from_hsv(0, 0, 1)).to eql @white
expect(from_hsv(100, 0, 1)).to eql @white
# Converting the "pure" colors should work
expect(from_hsv(0, 1, 1)).to eql @red
expect(from_hsv(120, 1, 1)).to eql @green
expect(from_hsv(240, 1, 1)).to eql @blue
# And, finally, one random color
expect(from_hsv(120, 0.5, 0.80)).to eql 0x66cc66ff
# Hue 0 and hue 360 should be equivalent
expect(from_hsv(0, 0.5, 0.5)).to eql from_hsv(360, 0.5, 0.5)
expect(from_hsv(0, 0.5, 0.5)).to eql from_hsv(360.0, 0.5, 0.5)
end
it "should optionally accept a fourth param for alpha" do
expect(from_hsv(0, 1, 1, 255)).to eql @red
expect(from_hsv(120, 1, 1, 255)).to eql @green
expect(from_hsv(240, 1, 1, 255)).to eql @blue
expect(from_hsv(0, 1, 1, 0)).to eql 0xff000000 # transparent red
expect(from_hsv(120, 1, 1, 0)).to eql 0x00ff0000 # transparent green
expect(from_hsv(240, 1, 1, 0)).to eql 0x0000ff00 # transparent blue
end
end
describe "#from_hsl" do
it "should load colors correctly from an HSL triple" do
# At 0 lightness, should always be black
expect(from_hsl(0, 0, 0)).to eql @black
expect(from_hsl(100, 0, 0)).to eql @black
expect(from_hsl(54, 0.5, 0)).to eql @black
# At 1 lightness, should always be white
expect(from_hsl(0, 0, 1)).to eql @white
expect(from_hsl(0, 0.5, 1)).to eql @white
expect(from_hsl(110, 0, 1)).to eql @white
# 'Pure' colors should work
expect(from_hsl(0, 1, 0.5)).to eql @red
expect(from_hsl(120, 1, 0.5)).to eql @green
expect(from_hsl(240, 1, 0.5)).to eql @blue
# Random colors
expect(from_hsl(87.27, 0.5, 0.5686)).to eql 0x95c759ff
expect(from_hsl(271.83, 0.5399, 0.4176)).to eql 0x6d30a3ff
expect(from_hsl(63.6, 0.5984, 0.4882)).to eql 0xbec631ff
# Hue 0 and hue 360 should be equivalent
expect(from_hsl(0, 0.5, 0.5)).to eql from_hsl(360, 0.5, 0.5)
expect(from_hsl(0, 0.5, 0.5)).to eql from_hsl(360.0, 0.5, 0.5)
end
it "should optionally accept a fourth param for alpha" do
expect(from_hsl(0, 1, 0.5, 255)).to eql @red
expect(from_hsl(120, 1, 0.5, 255)).to eql @green
expect(from_hsl(240, 1, 0.5, 255)).to eql @blue
expect(from_hsl(0, 1, 0.5, 0)).to eql 0xff000000 # transparent red
expect(from_hsl(120, 1, 0.5, 0)).to eql 0x00ff0000 # transparent green
expect(from_hsl(240, 1, 0.5, 0)).to eql 0x0000ff00 # transparent blue
end
end
describe "#html_color" do
it "should find the correct color value" do
expect(html_color(:springgreen)).to eql 0x00ff7fff
expect(html_color(:spring_green)).to eql 0x00ff7fff
expect(html_color("springgreen")).to eql 0x00ff7fff
expect(html_color("spring green")).to eql 0x00ff7fff
expect(html_color("SpringGreen")).to eql 0x00ff7fff
expect(html_color("SPRING_GREEN")).to eql 0x00ff7fff
end
it "should set the opacity level explicitly" do
expect(html_color(:springgreen, 0xff)).to eql 0x00ff7fff
expect(html_color(:springgreen, 0xaa)).to eql 0x00ff7faa
expect(html_color(:springgreen, 0x00)).to eql 0x00ff7f00
end
it "should set opacity levels from the color name" do
expect(html_color("Spring green @ 1.0")).to eql 0x00ff7fff
expect(html_color("Spring green @ 0.666")).to eql 0x00ff7faa
expect(html_color("Spring green @ 0.0")).to eql 0x00ff7f00
end
it "should raise for an unkown color name" do
expect { html_color(:nonsense) }.to raise_error(ArgumentError)
end
end
describe "#opaque?" do
it "should correctly check for opaqueness" do
expect(opaque?(@white)).to eql true
expect(opaque?(@black)).to eql true
expect(opaque?(@opaque)).to eql true
expect(opaque?(@non_opaque)).to eql false
expect(opaque?(@fully_transparent)).to eql false
end
end
describe "extraction of separate color channels" do
it "should extract components from a color correctly" do
expect(r(@opaque)).to eql 10
expect(g(@opaque)).to eql 100
expect(b(@opaque)).to eql 150
expect(a(@opaque)).to eql 255
end
end
describe "#grayscale_teint" do
it "should calculate the correct grayscale teint" do
expect(grayscale_teint(@opaque)).to eql 79
expect(grayscale_teint(@non_opaque)).to eql 79
end
end
describe "#to_grayscale" do
it "should use the grayscale teint for r, g and b" do
gs = to_grayscale(@non_opaque)
expect(r(gs)).to eql grayscale_teint(@non_opaque)
expect(g(gs)).to eql grayscale_teint(@non_opaque)
expect(b(gs)).to eql grayscale_teint(@non_opaque)
end
it "should preserve the alpha channel" do
expect(a(to_grayscale(@non_opaque))).to eql a(@non_opaque)
expect(a(to_grayscale(@opaque))).to eql ChunkyPNG::Color::MAX
end
end
describe "#to_hex" do
it "should represent colors correcly using hex notation" do
expect(to_hex(@white)).to eql "#ffffffff"
expect(to_hex(@black)).to eql "#000000ff"
expect(to_hex(@opaque)).to eql "#0a6496ff"
expect(to_hex(@non_opaque)).to eql "#0a649664"
expect(to_hex(@fully_transparent)).to eql "#0a649600"
end
it "should represent colors correcly using hex notation without alpha channel" do
expect(to_hex(@white, false)).to eql "#ffffff"
expect(to_hex(@black, false)).to eql "#000000"
expect(to_hex(@opaque, false)).to eql "#0a6496"
expect(to_hex(@non_opaque, false)).to eql "#0a6496"
expect(to_hex(@fully_transparent, false)).to eql "#0a6496"
end
end
describe "#to_hsv" do
it "should return a [hue, saturation, value] array" do
expect(to_hsv(@white)).to eql [0, 0.0, 1.0]
expect(to_hsv(@black)).to eql [0, 0.0, 0.0]
expect(to_hsv(@red)).to eql [0, 1.0, 1.0]
expect(to_hsv(@blue)).to eql [240, 1.0, 1.0]
expect(to_hsv(@green)).to eql [120, 1.0, 1.0]
expect(to_hsv(0x805440ff)[0]).to be_within(1).of(19)
expect(to_hsv(0x805440ff)[1]).to be_within(0.01).of(0.5)
expect(to_hsv(0x805440ff)[2]).to be_within(0.01).of(0.5)
end
it "should optionally include the alpha channel" do
expect(to_hsv(@white, true)).to eql [0, 0.0, 1.0, 255]
expect(to_hsv(@red, true)).to eql [0, 1.0, 1.0, 255]
expect(to_hsv(@blue, true)).to eql [240, 1.0, 1.0, 255]
expect(to_hsv(@green, true)).to eql [120, 1.0, 1.0, 255]
expect(to_hsv(@opaque, true)[3]).to eql 255
expect(to_hsv(@fully_transparent, true)[3]).to eql 0
end
end
describe "#to_hsl" do
it "should return a [hue, saturation, lightness] array" do
expect(to_hsl(@white)).to eql [0, 0.0, 1.0]
expect(to_hsl(@black)).to eql [0, 0.0, 0.0]
expect(to_hsl(@red)).to eql [0, 1.0, 0.5]
expect(to_hsl(@blue)).to eql [240, 1.0, 0.5]
expect(to_hsl(@green)).to eql [120, 1.0, 0.5]
end
it "should optionally include the alpha channel in the returned array" do
expect(to_hsl(@white, true)).to eql [0, 0.0, 1.0, 255]
expect(to_hsl(@black, true)).to eql [0, 0.0, 0.0, 255]
expect(to_hsl(@red, true)).to eql [0, 1.0, 0.5, 255]
expect(to_hsl(@blue, true)).to eql [240, 1.0, 0.5, 255]
expect(to_hsl(@green, true)).to eql [120, 1.0, 0.5, 255]
expect(to_hsl(@opaque, true)[3]).to eql 255
expect(to_hsl(@fully_transparent, true)[3]).to eql 0
end
end
describe "conversion to other formats" do
it "should convert the individual color values back correctly" do
expect(to_truecolor_bytes(@opaque)).to eql [10, 100, 150]
expect(to_truecolor_alpha_bytes(@non_opaque)).to eql [10, 100, 150, 100]
end
end
describe "#compose" do
it "should use the foregorund color as is when the background color is fully transparent" do
expect(compose(@non_opaque, @fully_transparent)).to eql @non_opaque
end
it "should use the foregorund color as is when an opaque color is given as foreground color" do
expect(compose(@opaque, @white)).to eql @opaque
end
it "should use the background color as is when a fully transparent pixel is given as foreground color" do
expect(compose(@fully_transparent, @white)).to eql @white
end
it "should compose pixels correctly with both algorithms" do
expect(compose_quick(@non_opaque, @white)).to eql 0x9fc2d6ff
expect(compose_precise(@non_opaque, @white)).to eql 0x9fc2d6ff
end
end
describe "#decompose_alpha" do
it "should decompose the alpha channel correctly" do
expect(decompose_alpha(0x9fc2d6ff, @opaque, @white)).to eql 0x00000064
end
it "should return fully transparent if the background channel matches the resulting color" do
expect(decompose_alpha(0xabcdefff, 0xff000000, 0xabcdefff)).to eql 0x00
end
it "should return fully opaque if the background channel matches the mask color" do
expect(decompose_alpha(0xff000000, 0xabcdefff, 0xabcdefff)).to eql 0xff
end
it "should return fully opaque if the resulting color matches the mask color" do
expect(decompose_alpha(0xabcdefff, 0xabcdefff, 0xffffffff)).to eql 255
end
end
describe "#blend" do
it "should blend colors correctly" do
expect(blend(@opaque, @black)).to eql 0x05324bff
end
it "should not matter what color is used as foreground, and what as background" do
expect(blend(@opaque, @black)).to eql blend(@black, @opaque)
end
end
describe "#euclidean_distance_rgba" do
subject { euclidean_distance_rgba(color_a, color_b) }
context "with white and black" do
let(:color_a) { @white }
let(:color_b) { @black }
it { should == Math.sqrt(195_075) } # sqrt(255^2 * 3)
end
context "with black and white" do
let(:color_a) { @black }
let(:color_b) { @white }
it { should == Math.sqrt(195_075) } # sqrt(255^2 * 3)
end
context "with the same colors" do
let(:color_a) { @white }
let(:color_b) { @white }
it { should == 0 }
end
end
end
chunky_png-1.3.15/spec/chunky_png/point_spec.rb 0000644 0001750 0001750 00000005343 13766004353 021023 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Point do
subject { ChunkyPNG::Point.new(1, 2) }
it { should respond_to(:x) }
it { should respond_to(:y) }
describe "#within_bounds?" do
it { should be_within_bounds(2, 3) }
it { should_not be_within_bounds("1x3") }
it { should_not be_within_bounds(2, 2) }
it { should_not be_within_bounds("[1 2]") }
end
describe "#<=>" do
it "should return 0 if the coordinates are identical" do
expect((subject <=> ChunkyPNG::Point.new(1, 2))).to eql(0)
end
it "should return -1 if the y coordinate is smaller than the other one" do
expect((subject <=> ChunkyPNG::Point.new(1, 3))).to eql(-1)
expect((subject <=> ChunkyPNG::Point.new(0, 3))).to eql(-1) # x doesn't matter
expect((subject <=> ChunkyPNG::Point.new(2, 3))).to eql(-1) # x doesn't matter
end
it "should return 1 if the y coordinate is larger than the other one" do
expect((subject <=> ChunkyPNG::Point.new(1, 0))).to eql(1)
expect((subject <=> ChunkyPNG::Point.new(0, 0))).to eql(1) # x doesn't matter
expect((subject <=> ChunkyPNG::Point.new(2, 0))).to eql(1) # x doesn't matter
end
it "should return -1 if the x coordinate is smaller and y is the same" do
expect((subject <=> ChunkyPNG::Point.new(2, 2))).to eql(-1)
end
it "should return 1 if the x coordinate is larger and y is the same" do
expect((subject <=> ChunkyPNG::Point.new(0, 2))).to eql(1)
end
end
end
describe "ChunkyPNG.Point" do
subject { ChunkyPNG::Point.new(1, 2) }
it "should create a point from a 2-item array" do
expect(ChunkyPNG::Point([1, 2])).to eql subject
expect(ChunkyPNG::Point(["1", "2"])).to eql subject
end
it "should create a point from a hash with x and y keys" do
expect(ChunkyPNG::Point(x: 1, y: 2)).to eql subject
expect(ChunkyPNG::Point("x" => "1", "y" => "2")).to eql subject
end
it "should create a point from a ChunkyPNG::Dimension object" do
dimension = ChunkyPNG::Dimension.new(1, 2)
ChunkyPNG::Point(dimension) == subject
end
it "should create a point from a point-like string" do
[
ChunkyPNG::Point("1,2"),
ChunkyPNG::Point("1 2"),
ChunkyPNG::Point("(1 , 2)"),
ChunkyPNG::Point("{1,\t2}"),
ChunkyPNG::Point("[1 2}"),
].all? { |point| point == subject }
end
it "should create a point from an object that responds to x and y" do
mock_object = Struct.new(:x, :y).new(1, 2)
expect(ChunkyPNG::Point(mock_object)).to eql subject
end
it "should raise an exception if the input is not understood" do
expect { ChunkyPNG::Point(Object.new) }.to raise_error(ArgumentError)
expect { ChunkyPNG::Point(1, 2, 3) }.to raise_error(ArgumentError)
end
end
chunky_png-1.3.15/spec/chunky_png/canvas_spec.rb 0000644 0001750 0001750 00000021404 13766004353 021141 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas do
subject { ChunkyPNG::Canvas.new(1, 1, ChunkyPNG::Color::WHITE) }
it { should respond_to(:width) }
it { should respond_to(:height) }
it { should respond_to(:pixels) }
describe "#initialize" do
it "should accept a single color value as background color" do
canvas = ChunkyPNG::Canvas.new(2, 2, "red @ 0.8")
expect(canvas[1, 0]).to eql ChunkyPNG::Color.parse("red @ 0.8")
end
it "should raise an error if the color value is not understood" do
expect { ChunkyPNG::Canvas.new(2, 2, :nonsense) }.to raise_error(ArgumentError)
end
it "should accept an array as initial pixel values" do
canvas = ChunkyPNG::Canvas.new(2, 2, [1, 2, 3, 4])
expect(canvas[0, 0]).to eql 1
expect(canvas[1, 0]).to eql 2
expect(canvas[0, 1]).to eql 3
expect(canvas[1, 1]).to eql 4
end
it "should raise an ArgumentError if the initial array does not have the correct number of elements" do
expect { ChunkyPNG::Canvas.new(2, 2, [1, 2, 3]) }.to raise_error(ArgumentError)
expect { ChunkyPNG::Canvas.new(2, 2, [1, 2, 3, 4, 5]) }.to raise_error(ArgumentError)
end
it "should use a transparent background by default" do
canvas = ChunkyPNG::Canvas.new(1, 1)
expect(canvas[0, 0]).to eql ChunkyPNG::Color::TRANSPARENT
end
end
describe "#dimension" do
it "should return the dimensions as a Dimension instance" do
expect(subject.dimension).to eql ChunkyPNG::Dimension("1x1")
end
end
describe "#area" do
it "should return the dimensions as two-item array" do
expect(subject.area).to eql ChunkyPNG::Dimension("1x1").area
end
end
describe "#include?" do
it "should return true if the coordinates are within bounds, false otherwise" do
# rubocop:disable Layout/SpaceInsideParens
expect(subject.include_xy?( 0, 0)).to eql true
expect(subject.include_xy?(-1, 0)).to eql false
expect(subject.include_xy?( 1, 0)).to eql false
expect(subject.include_xy?( 0, -1)).to eql false
expect(subject.include_xy?( 0, 1)).to eql false
expect(subject.include_xy?(-1, -1)).to eql false
expect(subject.include_xy?(-1, 1)).to eql false
expect(subject.include_xy?( 1, -1)).to eql false
expect(subject.include_xy?( 1, 1)).to eql false
# rubocop:enable Layout/SpaceInsideParens
end
it "should accept strings, arrays, hashes and points as well" do
expect(subject).to include("0, 0")
expect(subject).to_not include("0, 1")
expect(subject).to include([0, 0])
expect(subject).to_not include([0, 1])
expect(subject).to include(y: 0, x: 0)
expect(subject).to_not include(y: 1, x: 0)
expect(subject).to include(ChunkyPNG::Point.new(0, 0))
expect(subject).to_not include(ChunkyPNG::Point.new(0, 1))
end
end
describe "#include_x?" do
it "should return true if the x-coordinate is within bounds, false otherwise" do
expect(subject.include_x?(0)).to eql true
expect(subject.include_x?(-1)).to eql false
expect(subject.include_x?(1)).to eql false
end
end
describe "#include_y?" do
it "should return true if the y-coordinate is within bounds, false otherwise" do
expect(subject.include_y?(0)).to eql true
expect(subject.include_y?(-1)).to eql false
expect(subject.include_y?(1)).to eql false
end
end
describe "#assert_xy!" do
it "should not raise an exception if the coordinates are within bounds" do
expect(subject).to receive(:include_xy?).with(0, 0).and_return(true)
expect { subject.send(:assert_xy!, 0, 0) }.to_not raise_error
end
it "should raise an exception if the coordinates are out of bounds bounds" do
expect(subject).to receive(:include_xy?).with(0, -1).and_return(false)
expect { subject.send(:assert_xy!, 0, -1) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
describe "#assert_x!" do
it "should not raise an exception if the x-coordinate is within bounds" do
expect(subject).to receive(:include_x?).with(0).and_return(true)
expect { subject.send(:assert_x!, 0) }.to_not raise_error
end
it "should raise an exception if the x-coordinate is out of bounds bounds" do
expect(subject).to receive(:include_y?).with(-1).and_return(false)
expect { subject.send(:assert_y!, -1) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
describe "#[]" do
it "should return the pixel value if the coordinates are within bounds" do
expect(subject[0, 0]).to eql ChunkyPNG::Color::WHITE
end
it "should assert the coordinates to be within bounds" do
expect(subject).to receive(:assert_xy!).with(0, 0)
subject[0, 0]
end
end
describe "#get_pixel" do
it "should return the pixel value if the coordinates are within bounds" do
expect(subject.get_pixel(0, 0)).to eql ChunkyPNG::Color::WHITE
end
it "should not assert nor check the coordinates" do
expect(subject).to_not receive(:assert_xy!)
expect(subject).to_not receive(:include_xy?)
subject.get_pixel(0, 0)
end
end
describe "#[]=" do
it "should change the pixel's color value" do
expect { subject[0, 0] = ChunkyPNG::Color::BLACK }.to change { subject[0, 0] }
.from(ChunkyPNG::Color::WHITE)
.to(ChunkyPNG::Color::BLACK)
end
it "should assert the bounds of the image" do
expect(subject).to receive(:assert_xy!).with(0, 0)
subject[0, 0] = ChunkyPNG::Color::BLACK
end
end
describe "set_pixel" do
it "should change the pixel's color value" do
expect { subject.set_pixel(0, 0, ChunkyPNG::Color::BLACK) }.to change { subject[0, 0] }
.from(ChunkyPNG::Color::WHITE)
.to(ChunkyPNG::Color::BLACK)
end
it "should not assert or check the bounds of the image" do
expect(subject).to_not receive(:assert_xy!)
expect(subject).to_not receive(:include_xy?)
subject.set_pixel(0, 0, ChunkyPNG::Color::BLACK)
end
end
describe "#set_pixel_if_within_bounds" do
it "should change the pixel's color value" do
expect { subject.set_pixel_if_within_bounds(0, 0, ChunkyPNG::Color::BLACK) }.to change { subject[0, 0] }
.from(ChunkyPNG::Color::WHITE)
.to(ChunkyPNG::Color::BLACK)
end
it "should not assert, but only check the coordinates" do
expect(subject).to_not receive(:assert_xy!)
expect(subject).to receive(:include_xy?).with(0, 0)
subject.set_pixel_if_within_bounds(0, 0, ChunkyPNG::Color::BLACK)
end
it "should do nothing if the coordinates are out of bounds" do
expect(subject.set_pixel_if_within_bounds(-1, 1, ChunkyPNG::Color::BLACK)).to be_nil
expect(subject[0, 0]).to eql ChunkyPNG::Color::WHITE
end
end
describe "#row" do
before { @canvas = reference_canvas("operations") }
it "should give an out of bounds exception when y-coordinate is out of bounds" do
expect { @canvas.row(-1) }.to raise_error(ChunkyPNG::OutOfBounds)
expect { @canvas.row(16) }.to raise_error(ChunkyPNG::OutOfBounds)
end
it "should return the correct pixels" do
data = @canvas.row(0)
expect(data.length).to eql @canvas.width
expect(data).to eql [65535, 268500991, 536936447, 805371903, 1073807359, 1342242815, 1610678271, 1879113727, 2147549183, 2415984639, 2684420095, 2952855551, 3221291007, 3489726463, 3758161919, 4026597375]
end
end
describe "#column" do
before { @canvas = reference_canvas("operations") }
it "should give an out of bounds exception when x-coordinate is out of bounds" do
expect { @canvas.column(-1) }.to raise_error(ChunkyPNG::OutOfBounds)
expect { @canvas.column(16) }.to raise_error(ChunkyPNG::OutOfBounds)
end
it "should return the correct pixels" do
data = @canvas.column(0)
expect(data.length).to eql @canvas.height
expect(data).to eql [65535, 1114111, 2162687, 3211263, 4259839, 5308415, 6356991, 7405567, 8454143, 9502719, 10551295, 11599871, 12648447, 13697023, 14745599, 15794175]
end
end
describe "#replace_canvas" do
it "should change the dimension of the canvas" do
expect { subject.send(:replace_canvas!, 2, 2, [1, 2, 3, 4]) }.to change { subject.dimension }
.from(ChunkyPNG::Dimension("1x1"))
.to(ChunkyPNG::Dimension("2x2"))
end
it "should change the pixel array" do
expect { subject.send(:replace_canvas!, 2, 2, [1, 2, 3, 4]) }.to change { subject.pixels }
.from([ChunkyPNG::Color("white")])
.to([1, 2, 3, 4])
end
it "should return itself" do
expect(subject.send(:replace_canvas!, 2, 2, [1, 2, 3, 4])).to equal(subject)
end
end
describe "#inspect" do
it "should give a string description of the canvas" do
expect(subject.inspect).to eql ""
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/ 0000755 0001750 0001750 00000000000 13766004353 017601 5 ustar daniel daniel chunky_png-1.3.15/spec/chunky_png/canvas/stream_exporting_spec.rb 0000644 0001750 0001750 00000005460 13766004353 024537 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas do
describe "#to_rgba_stream" do
it "should export a sample canvas to an RGBA stream correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [
ChunkyPNG::Color.rgba(1, 2, 3, 4),
ChunkyPNG::Color.rgba(5, 6, 7, 8),
ChunkyPNG::Color.rgba(4, 3, 2, 1),
ChunkyPNG::Color.rgba(8, 7, 6, 5),
])
expect(canvas.to_rgba_stream).to eql [1, 2, 3, 4, 5, 6, 7, 8, 4, 3, 2, 1, 8, 7, 6, 5].pack("C16")
end
it "should export an image to an RGBA datastream correctly" do
expect(reference_canvas("pixelstream_reference").to_rgba_stream).to eql resource_data("pixelstream.rgba")
end
end
describe "#to_rgb_stream" do
it "should export a sample canvas to an RGBA stream correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [
ChunkyPNG::Color.rgba(1, 2, 3, 4),
ChunkyPNG::Color.rgba(5, 6, 7, 8),
ChunkyPNG::Color.rgba(4, 3, 2, 1),
ChunkyPNG::Color.rgba(8, 7, 6, 5),
])
expect(canvas.to_rgb_stream).to eql [1, 2, 3, 5, 6, 7, 4, 3, 2, 8, 7, 6].pack("C12")
end
it "should export an image to an RGB datastream correctly" do
expect(reference_canvas("pixelstream_reference").to_rgb_stream).to eql resource_data("pixelstream.rgb")
end
end
describe "#to_grayscale_stream" do
it "should export a grayscale image to a grayscale datastream correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [
ChunkyPNG::Color.grayscale(1),
ChunkyPNG::Color.grayscale(2),
ChunkyPNG::Color.grayscale(3),
ChunkyPNG::Color.grayscale(4),
])
expect(canvas.to_grayscale_stream).to eql [1, 2, 3, 4].pack("C4")
end
it "should export a color image to a grayscale datastream, using B values" do
canvas = ChunkyPNG::Canvas.new(2, 2, [
ChunkyPNG::Color.rgba(1, 2, 3, 4),
ChunkyPNG::Color.rgba(5, 6, 7, 8),
ChunkyPNG::Color.rgba(4, 3, 2, 1),
ChunkyPNG::Color.rgba(8, 7, 6, 5),
])
expect(canvas.to_grayscale_stream).to eql [3, 7, 2, 6].pack("C4")
end
end
describe "#to_alpha_channel_stream" do
it "should export an opaque image to an alpha channel datastream correctly" do
grayscale_array = Array.new(reference_canvas("pixelstream_reference").pixels.length, 255)
expect(reference_canvas("pixelstream_reference").to_alpha_channel_stream).to eql grayscale_array.pack("C*")
end
it "should export a transparent image to an alpha channel datastream correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [
ChunkyPNG::Color.rgba(1, 2, 3, 4),
ChunkyPNG::Color.rgba(5, 6, 7, 8),
ChunkyPNG::Color.rgba(4, 3, 2, 1),
ChunkyPNG::Color.rgba(8, 7, 6, 5),
])
expect(canvas.to_alpha_channel_stream).to eql [4, 8, 1, 5].pack("C4")
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/data_url_importing_spec.rb 0000644 0001750 0001750 00000000763 13766004353 025031 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas do
describe ".from_data_url" do
it "should import an image from a data URL" do
data_url = reference_canvas("operations").to_data_url
expect(ChunkyPNG::Canvas.from_data_url(data_url)).to eql reference_canvas("operations")
end
it "should raise an exception if the string is not a proper data URL" do
expect { ChunkyPNG::Canvas.from_data_url("whatever") }.to raise_error(ChunkyPNG::SignatureMismatch)
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/stream_importing_spec.rb 0000644 0001750 0001750 00000002001 13766004353 024514 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas do
describe ".from_rgb_stream" do
it "should load an image correctly from a datastream" do
File.open(resource_file("pixelstream.rgb")) do |stream|
matrix = ChunkyPNG::Canvas.from_rgb_stream(240, 180, stream)
expect(matrix).to eql reference_canvas("pixelstream_reference")
end
end
end
describe ".from_bgr_stream" do
it "should load an image correctly from a datastream" do
File.open(resource_file("pixelstream.bgr")) do |stream|
matrix = ChunkyPNG::Canvas.from_bgr_stream(240, 180, stream)
expect(matrix).to eql reference_canvas("pixelstream_reference")
end
end
end
describe ".from_rgba_stream" do
it "should load an image correctly from a datastream" do
File.open(resource_file("pixelstream.rgba")) do |stream|
matrix = ChunkyPNG::Canvas.from_rgba_stream(240, 180, stream)
expect(matrix).to eql reference_canvas("pixelstream_reference")
end
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/resampling_spec.rb 0000644 0001750 0001750 00000010533 13766004353 023303 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas::Resampling do
subject { reference_canvas("clock") }
describe "#resample_nearest_neighbor" do
it "should downscale from 2x2 to 1x1 correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [1, 2, 3, 4])
expect(canvas.resample_nearest_neighbor(1, 1)).to eql ChunkyPNG::Canvas.new(1, 1, [4])
end
it "should upscale from 2x2 to 4x4 correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [1, 2, 3, 4])
expect(canvas.resample_nearest_neighbor(4, 4)).to eql ChunkyPNG::Canvas.new(4, 4, [1, 1, 2, 2, 1, 1, 2, 2, 3, 3, 4, 4, 3, 3, 4, 4])
end
it "should upscale both axis of the image" do
expect(subject.resample_nearest_neighbor(45, 45)).to eql reference_canvas("clock_nn_xup_yup")
end
it "should downscale both axis of the image" do
expect(subject.resample_nearest_neighbor(12, 12)).to eql reference_canvas("clock_nn_xdown_ydown")
end
it "should downscale the x-axis and upscale the y-axis of the image" do
expect(subject.resample_nearest_neighbor(20, 50)).to eql reference_canvas("clock_nn_xdown_yup")
end
it "should not return itself" do
subject.resample_nearest_neighbor(1, 1).should_not equal(subject)
end
it "should not change the original image's dimensions" do
expect { subject.resample_nearest_neighbor(1, 1) }.to_not change { subject.dimension }
end
end
describe "#resample_nearest_neighbor!" do
it "should upscale both axis of the image" do
subject.resample_nearest_neighbor!(45, 45)
expect(subject).to eql reference_canvas("clock_nn_xup_yup")
end
it "should downscale both axis of the image" do
subject.resample_nearest_neighbor!(12, 12)
expect(subject).to eql reference_canvas("clock_nn_xdown_ydown")
end
it "should downscale the x-axis and upscale the y-axis of the image" do
subject.resample_nearest_neighbor!(20, 50)
expect(subject).to eql reference_canvas("clock_nn_xdown_yup")
end
it "should return itself" do
expect(subject.resample_nearest_neighbor!(1, 1)).to equal(subject)
end
it "should change the original image's dimensions" do
expect { subject.resample_nearest_neighbor!(1, 1) }.to change { subject.dimension }.to(ChunkyPNG::Dimension("1x1"))
end
end
describe "#resample_bilinear" do
it "should downscale from 2x2 to 1x1 correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [1, 2, 3, 4])
expect(canvas.resample_bilinear(1, 1)).to eql ChunkyPNG::Canvas.new(1, 1, [2])
end
it "should upscale from 2x2 to 4x4 correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [1, 2, 3, 4])
expect(canvas.resample_bilinear(4, 4)).to eql ChunkyPNG::Canvas.new(4, 4, [1, 2, 1, 2, 2, 2, 2, 2, 2, 3, 3, 4, 3, 3, 4, 4])
end
it "should upscale both axis of the image" do
expect(subject.resample_bilinear(45, 45)).to eql reference_canvas("clock_bl_xup_yup")
end
it "should downscale both axis of the image" do
expect(subject.resample_bilinear(12, 12)).to eql reference_canvas("clock_bl_xdown_ydown")
end
it "should downscale the x-axis and upscale the y-axis of the image" do
expect(subject.resample_bilinear(20, 50)).to eql reference_canvas("clock_bl_xdown_yup")
end
it "should not return itself" do
subject.resample_bilinear(1, 1).should_not equal(subject)
end
it "should not change the original image's dimensions" do
expect { subject.resample_bilinear(1, 1) }.to_not change { subject.dimension }
end
end
describe "#resample_bilinear!" do
it "should upscale both axis of the image" do
subject.resample_bilinear!(45, 45)
expect(subject).to eql reference_canvas("clock_bl_xup_yup")
end
it "should downscale both axis of the image" do
subject.resample_bilinear!(12, 12)
expect(subject).to eql reference_canvas("clock_bl_xdown_ydown")
end
it "should downscale the x-axis and upscale the y-axis of the image" do
subject.resample_bilinear!(20, 50)
expect(subject).to eql reference_canvas("clock_bl_xdown_yup")
end
it "should return itself" do
expect(subject.resample_bilinear!(1, 1)).to equal(subject)
end
it "should change the original image's dimensions" do
expect { subject.resample_bilinear!(1, 1) }.to change { subject.dimension }.to(ChunkyPNG::Dimension("1x1"))
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/adam7_interlacing_spec.rb 0000644 0001750 0001750 00000007101 13766004353 024507 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas::Adam7Interlacing do
include ChunkyPNG::Canvas::Adam7Interlacing
describe "#adam7_pass_sizes" do
it "should get the pass sizes for a 8x8 image correctly" do
expect(adam7_pass_sizes(8, 8)).to eql [
[1, 1], [1, 1], [2, 1], [2, 2], [4, 2], [4, 4], [8, 4],
]
end
it "should get the pass sizes for a 12x12 image correctly" do
expect(adam7_pass_sizes(12, 12)).to eql [
[2, 2], [1, 2], [3, 1], [3, 3], [6, 3], [6, 6], [12, 6],
]
end
it "should get the pass sizes for a 33x47 image correctly" do
expect(adam7_pass_sizes(33, 47)).to eql [
[5, 6], [4, 6], [9, 6], [8, 12], [17, 12], [16, 24], [33, 23],
]
end
it "should get the pass sizes for a 1x1 image correctly" do
expect(adam7_pass_sizes(1, 1)).to eql [
[1, 1], [0, 1], [1, 0], [0, 1], [1, 0], [0, 1], [1, 0],
]
end
it "should get the pass sizes for a 0x0 image correctly" do
expect(adam7_pass_sizes(0, 0)).to eql [
[0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0], [0, 0],
]
end
it "should always maintain the same amount of pixels in total" do
[[8, 8], [12, 12], [33, 47], [1, 1], [0, 0]].each do |(width, height)|
pass_sizes = adam7_pass_sizes(width, height)
expect(pass_sizes.inject(0) { |sum, (w, h)| sum + (w * h) }).to eql width * height
end
end
end
describe "#adam7_multiplier_offset" do
it "should get the multiplier and offset values for pass 1 correctly" do
expect(adam7_multiplier_offset(0)).to eql [3, 0, 3, 0]
end
it "should get the multiplier and offset values for pass 2 correctly" do
expect(adam7_multiplier_offset(1)).to eql [3, 4, 3, 0]
end
it "should get the multiplier and offset values for pass 3 correctly" do
expect(adam7_multiplier_offset(2)).to eql [2, 0, 3, 4]
end
it "should get the multiplier and offset values for pass 4 correctly" do
expect(adam7_multiplier_offset(3)).to eql [2, 2, 2, 0]
end
it "should get the multiplier and offset values for pass 5 correctly" do
expect(adam7_multiplier_offset(4)).to eql [1, 0, 2, 2]
end
it "should get the multiplier and offset values for pass 6 correctly" do
expect(adam7_multiplier_offset(5)).to eql [1, 1, 1, 0]
end
it "should get the multiplier and offset values for pass 7 correctly" do
expect(adam7_multiplier_offset(6)).to eql [0, 0, 1, 1]
end
end
describe "#adam7_merge_pass" do
it "should merge the submatrices correctly" do
submatrices = [
ChunkyPNG::Canvas.new(1, 1, 168430335), # r = 10
ChunkyPNG::Canvas.new(1, 1, 336860415), # r = 20
ChunkyPNG::Canvas.new(2, 1, 505290495), # r = 30
ChunkyPNG::Canvas.new(2, 2, 677668095), # r = 40
ChunkyPNG::Canvas.new(4, 2, 838912255), # r = 50
ChunkyPNG::Canvas.new(4, 4, 1023344895), # r = 60
ChunkyPNG::Canvas.new(8, 4, 1175063295), # r = 70
]
canvas = ChunkyPNG::Image.new(8, 8)
submatrices.each_with_index { |m, pass| adam7_merge_pass(pass, canvas, m) }
expect(canvas).to eql reference_image("adam7")
end
end
describe "#adam7_extract_pass" do
before(:each) { @canvas = reference_canvas("adam7") }
1.upto(7) do |pass|
it "should extract pass #{pass} correctly" do
sm = adam7_extract_pass(pass - 1, @canvas)
expect(sm.pixels.length).to eql sm.width * sm.height
expect(sm.pixels.uniq.length).to eql 1
expect(ChunkyPNG::Color.r(sm[0, 0])).to eql pass * 10
end
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/operations_spec.rb 0000644 0001750 0001750 00000031240 13766004353 023323 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas::Operations do
subject { reference_canvas("operations") }
describe "#grayscale" do
it "should not return itself" do
subject.grayscale.should_not equal(subject)
end
it "should convert the image correctly" do
expect(subject.grayscale).to eql reference_canvas("operations_grayscale")
end
it "should not adjust the current image" do
expect { subject.grayscale }.to_not change { subject.pixels }
end
end
describe "#grayscale!" do
it "should return itself" do
expect(subject.grayscale!).to equal(subject)
end
it "should convert the image correctly" do
subject.grayscale!
expect(subject).to eql reference_canvas("operations_grayscale")
end
end
describe "#crop" do
it "should crop the right pixels from the original canvas" do
expect(subject.crop(10, 5, 4, 8)).to eql reference_canvas("cropped")
end
it "should not return itself" do
subject.crop(10, 5, 4, 8).should_not equal(subject)
end
it "should not adjust the current image" do
expect { subject.crop(10, 5, 4, 8) }.to_not change { subject.pixels }
end
it "should raise an exception when the cropped image falls outside the oiginal image" do
expect { subject.crop(16, 16, 2, 2) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
describe "#crop!" do
context "when cropping both width and height" do
let(:crop_opts) { [10, 5, 4, 8] }
it "should crop the right pixels from the original canvas" do
subject.crop!(*crop_opts)
expect(subject).to eql reference_canvas("cropped")
end
it "should have a new width and height" do
expect { subject.crop!(*crop_opts) }.to change { subject.dimension }
.from(ChunkyPNG::Dimension("16x16"))
.to(ChunkyPNG::Dimension("4x8"))
end
it "should return itself" do
expect(subject.crop!(*crop_opts)).to equal(subject)
end
end
context "when cropping just the height" do
let(:crop_opts) { [0, 5, 16, 8] }
it "should crop the right pixels from the original canvas" do
subject.crop!(*crop_opts)
expect(subject).to eql reference_canvas("cropped_height")
end
it "should have a new width and height" do
expect { subject.crop!(*crop_opts) }.to change { subject.dimension }
.from(ChunkyPNG::Dimension("16x16"))
.to(ChunkyPNG::Dimension("16x8"))
end
it "should return itself" do
expect(subject.crop!(*crop_opts)).to equal(subject)
end
end
context "when the cropped image falls outside the original image" do
it "should raise an exception" do
expect { subject.crop!(16, 16, 2, 2) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
end
describe "#compose" do
it "should compose pixels correctly" do
subcanvas = ChunkyPNG::Canvas.new(4, 8, ChunkyPNG::Color.rgba(0, 0, 0, 75))
expect(subject.compose(subcanvas, 8, 4)).to eql reference_canvas("composited")
end
it "should leave the original intact" do
subject.compose(ChunkyPNG::Canvas.new(1, 1))
expect(subject).to eql reference_canvas("operations")
end
it "should not return itself" do
subject.compose(ChunkyPNG::Canvas.new(1, 1)).should_not equal(subject)
end
it "should raise an exception when the pixels to compose fall outside the image" do
expect { subject.compose(ChunkyPNG::Canvas.new(1, 1), 16, 16) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
describe "#compose!" do
it "should compose pixels correctly" do
subcanvas = ChunkyPNG::Canvas.new(4, 8, ChunkyPNG::Color.rgba(0, 0, 0, 75))
subject.compose!(subcanvas, 8, 4)
expect(subject).to eql reference_canvas("composited")
end
it "should return itself" do
expect(subject.compose!(ChunkyPNG::Canvas.new(1, 1))).to equal(subject)
end
it "should compose a base image and mask correctly" do
base = reference_canvas("clock_base")
mask = reference_canvas("clock_mask_updated")
base.compose!(mask)
expect(base).to eql reference_canvas("clock_updated")
end
it "should raise an exception when the pixels to compose fall outside the image" do
expect { subject.compose!(ChunkyPNG::Canvas.new(1, 1), 16, 16) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
describe "#replace" do
it "should replace the correct pixels" do
subcanvas = ChunkyPNG::Canvas.new(3, 2, ChunkyPNG::Color.rgb(200, 255, 0))
expect(subject.replace(subcanvas, 5, 4)).to eql reference_canvas("replaced")
end
it "should not return itself" do
subject.replace(ChunkyPNG::Canvas.new(1, 1)).should_not equal(subject)
end
it "should leave the original intact" do
subject.replace(ChunkyPNG::Canvas.new(1, 1))
expect(subject).to eql reference_canvas("operations")
end
it "should raise an exception when the pixels to replace fall outside the image" do
expect { subject.replace(ChunkyPNG::Canvas.new(1, 1), 16, 16) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
describe "#replace!" do
it "should replace the correct pixels" do
subcanvas = ChunkyPNG::Canvas.new(3, 2, ChunkyPNG::Color.rgb(200, 255, 0))
subject.replace!(subcanvas, 5, 4)
expect(subject).to eql reference_canvas("replaced")
end
it "should return itself" do
expect(subject.replace!(ChunkyPNG::Canvas.new(1, 1))).to equal(subject)
end
it "should raise an exception when the pixels to replace fall outside the image" do
expect { subject.replace!(ChunkyPNG::Canvas.new(1, 1), 16, 16) }.to raise_error(ChunkyPNG::OutOfBounds)
end
end
end
describe ChunkyPNG::Canvas::Operations do
subject { ChunkyPNG::Canvas.new(2, 3, [1, 2, 3, 4, 5, 6]) }
describe "#flip_horizontally!" do
it "should flip the pixels horizontally in place" do
subject.flip_horizontally!
expect(subject).to eql ChunkyPNG::Canvas.new(2, 3, [5, 6, 3, 4, 1, 2])
end
it "should return itself" do
expect(subject.flip_horizontally!).to equal(subject)
end
end
describe "#flip_horizontally" do
it "should flip the pixels horizontally" do
expect(subject.flip_horizontally).to eql ChunkyPNG::Canvas.new(2, 3, [5, 6, 3, 4, 1, 2])
end
it "should not return itself" do
subject.flip_horizontally.should_not equal(subject)
end
it "should return a copy of itself when applied twice" do
expect(subject.flip_horizontally.flip_horizontally).to eql subject
end
end
describe "#flip_vertically!" do
it "should flip the pixels vertically" do
subject.flip_vertically!
expect(subject).to eql ChunkyPNG::Canvas.new(2, 3, [2, 1, 4, 3, 6, 5])
end
it "should return itself" do
expect(subject.flip_horizontally!).to equal(subject)
end
end
describe "#flip_vertically" do
it "should flip the pixels vertically" do
expect(subject.flip_vertically).to eql ChunkyPNG::Canvas.new(2, 3, [2, 1, 4, 3, 6, 5])
end
it "should not return itself" do
subject.flip_horizontally.should_not equal(subject)
end
it "should return a copy of itself when applied twice" do
expect(subject.flip_vertically.flip_vertically).to eql subject
end
end
describe "#rotate_left" do
it "should rotate the pixels 90 degrees counter-clockwise" do
expect(subject.rotate_left).to eql ChunkyPNG::Canvas.new(3, 2, [2, 4, 6, 1, 3, 5])
end
it "should not return itself" do
subject.rotate_left.should_not equal(subject)
end
it "should not change the image dimensions" do
expect { subject.rotate_left }.to_not change { subject.dimension }
end
it "it should rotate 180 degrees when applied twice" do
expect(subject.rotate_left.rotate_left).to eql subject.rotate_180
end
it "it should rotate right when applied three times" do
expect(subject.rotate_left.rotate_left.rotate_left).to eql subject.rotate_right
end
it "should return itself when applied four times" do
expect(subject.rotate_left.rotate_left.rotate_left.rotate_left).to eql subject
end
end
describe "#rotate_left!" do
it "should rotate the pixels 90 degrees clockwise" do
subject.rotate_left!
expect(subject).to eql ChunkyPNG::Canvas.new(3, 2, [2, 4, 6, 1, 3, 5])
end
it "should return itself" do
expect(subject.rotate_left!).to equal(subject)
end
it "should change the image dimensions" do
expect { subject.rotate_left! }.to change { subject.dimension }
.from(ChunkyPNG::Dimension("2x3")).to(ChunkyPNG::Dimension("3x2"))
end
end
describe "#rotate_right" do
it "should rotate the pixels 90 degrees clockwise" do
expect(subject.rotate_right).to eql ChunkyPNG::Canvas.new(3, 2, [5, 3, 1, 6, 4, 2])
end
it "should not return itself" do
subject.rotate_right.should_not equal(subject)
end
it "should not change the image dimensions" do
expect { subject.rotate_right }.to_not change { subject.dimension }
end
it "it should rotate 180 degrees when applied twice" do
expect(subject.rotate_right.rotate_right).to eql subject.rotate_180
end
it "it should rotate left when applied three times" do
expect(subject.rotate_right.rotate_right.rotate_right).to eql subject.rotate_left
end
it "should return itself when applied four times" do
expect(subject.rotate_right.rotate_right.rotate_right.rotate_right).to eql subject
end
end
describe "#rotate_right!" do
it "should rotate the pixels 90 degrees clockwise" do
subject.rotate_right!
expect(subject).to eql ChunkyPNG::Canvas.new(3, 2, [5, 3, 1, 6, 4, 2])
end
it "should return itself" do
expect(subject.rotate_right!).to equal(subject)
end
it "should change the image dimensions" do
expect { subject.rotate_right! }.to change { subject.dimension }
.from(ChunkyPNG::Dimension("2x3"))
.to(ChunkyPNG::Dimension("3x2"))
end
end
describe "#rotate_180" do
it "should rotate the pixels 180 degrees" do
expect(subject.rotate_180).to eql ChunkyPNG::Canvas.new(2, 3, [6, 5, 4, 3, 2, 1])
end
it "should return not itself" do
subject.rotate_180.should_not equal(subject)
end
it "should return a copy of itself when applied twice" do
expect(subject.rotate_180.rotate_180).to eql subject
end
end
describe "#rotate_180!" do
it "should rotate the pixels 180 degrees" do
subject.rotate_180!
expect(subject).to eql ChunkyPNG::Canvas.new(2, 3, [6, 5, 4, 3, 2, 1])
end
it "should return itself" do
expect(subject.rotate_180!).to equal(subject)
end
end
end
describe ChunkyPNG::Canvas::Operations do
subject { ChunkyPNG::Canvas.new(4, 4).rect(1, 1, 2, 2, 255, 255) }
describe "#trim" do
it "should trim the border" do
expect(subject.trim).to eql ChunkyPNG::Canvas.new(2, 2, 255)
end
it "should not return itself" do
subject.trim.should_not equal(subject)
end
it "should be able to fail to trim a specified color" do
expect { subject.trim(ChunkyPNG::Color::BLACK) }.to_not change { subject.pixels }
end
it "should be the same after trimming an added border" do
expect(subject.border(2).trim).to eql subject
end
end
describe "#trim!" do
it "should trim the border" do
subject.trim!
expect(subject).to eql ChunkyPNG::Canvas.new(2, 2, 255)
end
it "should return itself" do
expect(subject.trim!).to equal(subject)
end
it "should change the image dimensions" do
expect { subject.trim! }.to change { subject.dimension }
.from(ChunkyPNG::Dimension("4x4"))
.to(ChunkyPNG::Dimension("2x2"))
end
end
end
describe ChunkyPNG::Canvas::Operations do
subject { ChunkyPNG::Canvas.new(4, 4) }
describe "#border" do
it "should add the border" do
expect(subject.border(2)).to eql reference_canvas("operations_border")
end
it "should not return itself" do
subject.border(1).should_not equal(subject)
end
it "should retain transparency" do
expect(ChunkyPNG::Canvas.new(1, 1).border(1).pixels).to include(0)
end
end
describe "#border!" do
it "should add the border" do
subject.border!(2)
expect(subject).to eql reference_canvas("operations_border")
end
it "should return itself" do
expect(subject.border!(1)).to equal(subject)
end
it "should retain transparency" do
subject.border!(1)
expect(subject.pixels).to include(0)
end
it "should change the image dimensions" do
expect { subject.border!(1) }.to change { subject.dimension }
.from(ChunkyPNG::Dimension("4x4"))
.to(ChunkyPNG::Dimension("6x6"))
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/masking_spec.rb 0000644 0001750 0001750 00000003215 13766004353 022572 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas::Masking do
subject { reference_canvas("clock") }
before(:all) do
@theme_color = ChunkyPNG::Color("#e10f7a")
@new_color = ChunkyPNG::Color("#ff0000")
@background_color = ChunkyPNG::Color("white")
end
describe "#change_theme_color!" do
it "should change the theme color correctly" do
subject.change_theme_color!(@theme_color, @new_color)
expect(subject).to eql reference_canvas("clock_updated")
end
end
describe "#extract_mask" do
it "should create the correct base and mask image" do
base, mask = subject.extract_mask(@theme_color, @background_color)
expect(base).to eql reference_canvas("clock_base")
expect(mask).to eql reference_canvas("clock_mask")
end
it "should create a mask image with only one opaque color" do
_, mask = subject.extract_mask(@theme_color, @background_color)
expect(mask.palette.opaque_palette.size).to eql 1
end
end
describe "#change_mask_color!" do
before { @mask = reference_canvas("clock_mask") }
it "should replace the mask color correctly" do
@mask.change_mask_color!(@new_color)
expect(@mask).to eql reference_canvas("clock_mask_updated")
end
it "should still only have one opaque color" do
@mask.change_mask_color!(@new_color)
expect(@mask.palette.opaque_palette.size).to eql 1
end
it "should raise an exception when the mask image has more than once color" do
not_a_mask = reference_canvas("operations")
expect { not_a_mask.change_mask_color!(@new_color) }.to raise_error(ChunkyPNG::ExpectationFailed)
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/png_decoding_spec.rb 0000644 0001750 0001750 00000010666 13766004353 023571 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas::PNGDecoding do
include ChunkyPNG::Canvas::PNGDecoding
describe "#decode_png_scanline" do
it "should decode a line without filtering as is" do
stream = [ChunkyPNG::FILTER_NONE, 255, 255, 255, 255, 255, 255, 255, 255, 255].pack("C*")
decode_png_str_scanline(stream, 0, nil, 9, 3)
expect(stream.unpack("@1C*")).to eql [255, 255, 255, 255, 255, 255, 255, 255, 255]
end
it "should decode a line with sub filtering correctly" do
# all white pixels
stream = [ChunkyPNG::FILTER_SUB, 255, 255, 255, 0, 0, 0, 0, 0, 0].pack("C*")
decode_png_str_scanline(stream, 0, nil, 9, 3)
expect(stream.unpack("@1C*")).to eql [255, 255, 255, 255, 255, 255, 255, 255, 255]
# all black pixels
stream = [ChunkyPNG::FILTER_SUB, 0, 0, 0, 0, 0, 0, 0, 0, 0].pack("C*")
decode_png_str_scanline(stream, 0, nil, 9, 3)
expect(stream.unpack("@1C*")).to eql [0, 0, 0, 0, 0, 0, 0, 0, 0]
# various colors
stream = [ChunkyPNG::FILTER_SUB, 255, 0, 45, 0, 255, 0, 112, 200, 178].pack("C*")
decode_png_str_scanline(stream, 0, nil, 9, 3)
expect(stream.unpack("@1C*")).to eql [255, 0, 45, 255, 255, 45, 111, 199, 223]
end
it "should decode a line with up filtering correctly" do
# previous line has various pixels
previous = [ChunkyPNG::FILTER_UP, 255, 255, 255, 127, 127, 127, 0, 0, 0]
current = [ChunkyPNG::FILTER_UP, 0, 127, 255, 0, 127, 255, 0, 127, 255]
stream = (previous + current).pack("C*")
decode_png_str_scanline(stream, 10, 0, 9, 3)
expect(stream.unpack("@11C9")).to eql [255, 126, 254, 127, 254, 126, 0, 127, 255]
end
it "should decode a line with average filtering correctly" do
previous = [ChunkyPNG::FILTER_AVERAGE, 10, 20, 30, 40, 50, 60, 70, 80, 80, 100, 110, 120] # rubocop:disable Layout/ExtraSpacing
current = [ChunkyPNG::FILTER_AVERAGE, 0, 0, 10, 23, 15, 13, 23, 63, 38, 60, 253, 53] # rubocop:disable Layout/ExtraSpacing
stream = (previous + current).pack("C*")
decode_png_str_scanline(stream, 13, 0, 12, 3)
expect(stream.unpack("@14C12")).to eql [5, 10, 25, 45, 45, 55, 80, 125, 105, 150, 114, 165]
end
it "should decode a line with paeth filtering correctly" do
previous = [ChunkyPNG::FILTER_PAETH, 10, 20, 30, 40, 50, 60, 70, 80, 80, 100, 110, 120] # rubocop:disable Layout/ExtraSpacing
current = [ChunkyPNG::FILTER_PAETH, 0, 0, 10, 20, 10, 0, 0, 40, 10, 20, 190, 0] # rubocop:disable Layout/ExtraSpacing
stream = (previous + current).pack("C*")
decode_png_str_scanline(stream, 13, 0, 12, 3)
expect(stream.unpack("@14C12")).to eql [10, 20, 40, 60, 60, 60, 70, 120, 90, 120, 54, 120]
end
end
describe "#decode_png_extract_4bit_value" do
it "should extract the high bits successfully" do
expect(decode_png_extract_4bit_value("10010110".to_i(2), 0)).to eql "1001".to_i(2)
end
it "should extract the low bits successfully" do
expect(decode_png_extract_4bit_value("10010110".to_i(2), 17)).to eql "0110".to_i(2)
end
end
describe "#decode_png_extract_2bit_value" do
it "should extract the first 2 bits successfully" do
expect(decode_png_extract_2bit_value("10010110".to_i(2), 0)).to eql "10".to_i(2)
end
it "should extract the second 2 bits successfully" do
expect(decode_png_extract_2bit_value("10010110".to_i(2), 5)).to eql "01".to_i(2)
end
it "should extract the third 2 bits successfully" do
expect(decode_png_extract_2bit_value("10010110".to_i(2), 2)).to eql "01".to_i(2)
end
it "should extract the low two bits successfully" do
expect(decode_png_extract_2bit_value("10010110".to_i(2), 7)).to eql "10".to_i(2)
end
end
describe "#decode_png_extract_1bit_value" do
it "should extract all separate bits correctly" do
expect(decode_png_extract_1bit_value("10010110".to_i(2), 0)).to eql 1
expect(decode_png_extract_1bit_value("10010110".to_i(2), 1)).to eql 0
expect(decode_png_extract_1bit_value("10010110".to_i(2), 2)).to eql 0
expect(decode_png_extract_1bit_value("10010110".to_i(2), 3)).to eql 1
expect(decode_png_extract_1bit_value("10010110".to_i(2), 4)).to eql 0
expect(decode_png_extract_1bit_value("10010110".to_i(2), 5)).to eql 1
expect(decode_png_extract_1bit_value("10010110".to_i(2), 6)).to eql 1
expect(decode_png_extract_1bit_value("10010110".to_i(2), 7)).to eql 0
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/drawing_spec.rb 0000644 0001750 0001750 00000016367 13766004353 022610 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas::Drawing do
describe "#compose_pixel" do
subject { ChunkyPNG::Canvas.new(1, 1, ChunkyPNG::Color.rgb(200, 150, 100)) }
it "should compose colors correctly" do
subject.compose_pixel(0, 0, ChunkyPNG::Color(100, 150, 200, 128))
expect(subject[0, 0]).to eql ChunkyPNG::Color(150, 150, 150)
end
it "should return the composed color" do
expect(subject.compose_pixel(0, 0, ChunkyPNG::Color.rgba(100, 150, 200, 128))).to eql ChunkyPNG::Color.rgb(150, 150, 150)
end
it "should do nothing when the coordinates are out of bounds" do
expect(subject.compose_pixel(1, -1, :black)).to be_nil
expect { subject.compose_pixel(1, -1, :black) }.to_not change { subject[0, 0] }
end
end
describe "#line" do
it "should draw lines correctly with anti-aliasing" do
canvas = ChunkyPNG::Canvas.new(31, 31, ChunkyPNG::Color::WHITE)
# rubocop:disable Layout/SpaceInsideParens # for improved readability
canvas.line( 0, 0, 30, 30, ChunkyPNG::Color::BLACK)
canvas.line( 0, 30, 30, 0, ChunkyPNG::Color::BLACK)
canvas.line(15, 30, 15, 0, ChunkyPNG::Color.rgba(200, 0, 0, 128))
canvas.line( 0, 15, 30, 15, ChunkyPNG::Color.rgba(200, 0, 0, 128))
canvas.line(30, 30, 0, 15, ChunkyPNG::Color.rgba( 0, 200, 0, 128), false)
canvas.line( 0, 15, 30, 0, ChunkyPNG::Color.rgba( 0, 200, 0, 128))
canvas.line( 0, 30, 15, 0, ChunkyPNG::Color.rgba( 0, 0, 200, 128), false)
canvas.line(15, 0, 30, 30, ChunkyPNG::Color.rgba( 0, 0, 200, 128))
# rubocop:enable Layout/SpaceInsideParens
expect(canvas).to eql reference_canvas("lines")
end
it "should draw partial lines if the coordinates are partially out of bounds" do
canvas = ChunkyPNG::Canvas.new(1, 2, ChunkyPNG::Color::WHITE)
canvas.line(-5, -5, 0, 0, "#000000")
expect(canvas.pixels).to eql [ChunkyPNG::Color::BLACK, ChunkyPNG::Color::WHITE]
end
it "should return itself to allow chaining" do
canvas = ChunkyPNG::Canvas.new(16, 16, ChunkyPNG::Color::WHITE)
expect(canvas.line(1, 1, 10, 10, :black)).to equal(canvas)
end
it "should draw a single pixel when the start and end point are the same" do
canvas = ChunkyPNG::Canvas.new(5, 5, ChunkyPNG::Color::WHITE)
canvas.line(2, 2, 2, 2, ChunkyPNG::Color::BLACK)
non_white_pixels = canvas.pixels.count { |pixel| pixel != ChunkyPNG::Color::WHITE }
expect(non_white_pixels).to eql 1
end
end
describe "#rect" do
subject { ChunkyPNG::Canvas.new(16, 16, "#ffffff") }
it "should draw a rectangle with the correct colors" do
subject.rect(1, 1, 10, 10, ChunkyPNG::Color.rgba(0, 255, 0, 80), ChunkyPNG::Color.rgba(255, 0, 0, 100))
subject.rect(5, 5, 14, 14, ChunkyPNG::Color.rgba(0, 0, 255, 160), ChunkyPNG::Color.rgba(255, 255, 0, 100))
expect(subject).to eql reference_canvas("rect")
end
it "should return itself to allow chaining" do
expect(subject.rect(1, 1, 10, 10)).to equal(subject)
end
it "should draw partial rectangles if the coordinates are partially out of bounds" do
subject.rect(0, 0, 20, 20, :black, :white)
expect(subject[0, 0]).to eql ChunkyPNG::Color::BLACK
end
it "should draw the rectangle fill only if the coordinates are fully out of bounds" do
subject.rect(-1, -1, 20, 20, :black, :white)
expect(subject[0, 0]).to eql ChunkyPNG::Color::WHITE
end
end
describe "#circle" do
subject { ChunkyPNG::Canvas.new(32, 32, ChunkyPNG::Color.rgba(0, 0, 255, 128)) }
it "should draw circles" do
subject.circle(11, 11, 10, ChunkyPNG::Color("red @ 0.5"), ChunkyPNG::Color("white @ 0.2"))
subject.circle(21, 21, 10, ChunkyPNG::Color("green @ 0.5"))
expect(subject).to eql reference_canvas("circles")
end
it "should draw partial circles when going of the canvas bounds" do
subject.circle(0, 0, 10, ChunkyPNG::Color(:red))
subject.circle(31, 16, 10, ChunkyPNG::Color(:black), ChunkyPNG::Color(:white, 0xaa))
expect(subject).to eql reference_canvas("partial_circles")
end
it "should return itself to allow chaining" do
expect(subject.circle(10, 10, 5, :red)).to equal(subject)
end
end
describe "#polygon" do
subject { ChunkyPNG::Canvas.new(22, 22) }
it "should draw an filled triangle when using 3 control points" do
subject.polygon("(2,2) (20,5) (5,20)", ChunkyPNG::Color(:black, 0xaa), ChunkyPNG::Color(:red, 0x44))
expect(subject).to eql reference_canvas("polygon_triangle_filled")
end
it "should draw a unfilled polygon with 6 control points" do
subject.polygon("(2,2) (12, 1) (20,5) (18,18) (5,20) (1,12)", ChunkyPNG::Color(:black))
expect(subject).to eql reference_canvas("polygon_unfilled")
end
it "should draw a vertically crossed filled polygon with 4 control points" do
subject.polygon("(2,2) (21,2) (2,21) (21,21)", ChunkyPNG::Color(:black), ChunkyPNG::Color(:red))
expect(subject).to eql reference_canvas("polygon_filled_vertical")
end
it "should draw a vertically crossed filled polygon with 4 control points" do
subject.polygon("(2,2) (2,21) (21,2) (21,21)", ChunkyPNG::Color(:black), ChunkyPNG::Color(:red))
expect(subject).to eql reference_canvas("polygon_filled_horizontal")
end
it "should return itself to allow chaining" do
expect(subject.polygon("(2,2) (20,5) (5,20)")).to equal(subject)
end
end
describe "#bezier_curve" do
subject { ChunkyPNG::Canvas.new(24, 24, ChunkyPNG::Color::WHITE) }
it "should draw a bezier curve starting at the first point" do
subject.bezier_curve("3,20 10,10, 20,20")
expect(subject[3, 20]).to eql ChunkyPNG::Color::BLACK
end
it "should draw a bezier curve ending at the last point" do
subject.bezier_curve("3,20 10,10, 20,20")
expect(subject[20, 20]).to eql ChunkyPNG::Color::BLACK
end
it "should draw a bezier curve with a color of green" do
subject.bezier_curve("3,20 10,10, 20,20", :green)
expect(subject[3, 20]).to eql ChunkyPNG::Color(:green)
end
it "should draw a three point bezier curve" do
expect(subject.bezier_curve("1,23 12,10 23,23")).to eql reference_canvas("bezier_three_point")
end
it "should draw a three point bezier curve flipped" do
expect(subject.bezier_curve("1,1 12,15 23,1")).to eql reference_canvas("bezier_three_point_flipped")
end
it "should draw a four point bezier curve" do
expect(subject.bezier_curve("1,23 1,5 22,5 22,23")).to eql reference_canvas("bezier_four_point")
end
it "should draw a four point bezier curve flipped" do
expect(subject.bezier_curve("1,1 1,19 22,19 22,1")).to eql reference_canvas("bezier_four_point_flipped")
end
it "should draw a four point bezier curve with a shape of an s" do
expect(subject.bezier_curve("1,23 1,5 22,23 22,5")).to eql reference_canvas("bezier_four_point_s")
end
it "should draw a five point bezier curve" do
expect(subject.bezier_curve("10,23 1,10 12,5 23,10 14,23")).to eql reference_canvas("bezier_five_point")
end
it "should draw a six point bezier curve" do
expect(subject.bezier_curve("1,23 4,15 8,20 2,2 23,15 23,1")).to eql reference_canvas("bezier_six_point")
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/data_url_exporting_spec.rb 0000644 0001750 0001750 00000001142 13766004353 025030 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas do
describe "#to_data_url" do
it "should export a sample canvas to an RGBA stream correctly" do
canvas = ChunkyPNG::Canvas.new(2, 2, [
ChunkyPNG::Color.rgba(1, 2, 3, 4),
ChunkyPNG::Color.rgba(5, 6, 7, 8),
ChunkyPNG::Color.rgba(4, 3, 2, 1),
ChunkyPNG::Color.rgba(8, 7, 6, 5),
])
expect(canvas.to_data_url).to eql "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACAgMAAAAP2OW3AAAADFBMVEUBAgMEAwIFBgcIBwazgAAdAAAABHRSTlMEAQgFhYDlfQAAAAxJREFUeJxjUmAKAAAAwAB1GNhIEwAAAABJRU5ErkJggg=="
end
end
end
chunky_png-1.3.15/spec/chunky_png/canvas/png_encoding_spec.rb 0000644 0001750 0001750 00000030325 13766004353 023575 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Canvas::PNGEncoding do
include ChunkyPNG::Canvas::PNGEncoding
context "determining encoding options" do
[:indexed, :grayscale, :grayscale_alpha, :truecolor, :truecolor_alpha].each do |color_mode_name|
it "should encode an image with color mode #{color_mode_name} correctly" do
canvas = ChunkyPNG::Canvas.new(10, 10, ChunkyPNG::Color.rgb(100, 100, 100))
color_mode = ChunkyPNG.const_get("COLOR_#{color_mode_name.to_s.upcase}")
blob = canvas.to_blob(color_mode: color_mode)
ds = ChunkyPNG::Datastream.from_blob(blob)
expect(ds.header_chunk.color).to eql color_mode
expect(ChunkyPNG::Canvas.from_datastream(ds)).to eql ChunkyPNG::Canvas.new(10, 10, ChunkyPNG::Color.rgb(100, 100, 100))
end
end
it "should encode an image with 2 colors using 1-bit indexed color mode" do
@canvas = ChunkyPNG::Canvas.from_file(png_suite_file("basic", "basn3p01.png"))
ds = ChunkyPNG::Datastream.from_blob(@canvas.to_blob)
expect(ds.header_chunk.color).to eql ChunkyPNG::COLOR_INDEXED
expect(ds.header_chunk.depth).to eql 1
expect(@canvas).to eql ChunkyPNG::Canvas.from_datastream(ds)
end
it "should encode an image with 4 colors using 2-bit indexed color mode" do
@canvas = ChunkyPNG::Canvas.from_file(png_suite_file("basic", "basn3p02.png"))
ds = ChunkyPNG::Datastream.from_blob(@canvas.to_blob)
expect(ds.header_chunk.color).to eql ChunkyPNG::COLOR_INDEXED
expect(ds.header_chunk.depth).to eql 2
expect(@canvas).to eql ChunkyPNG::Canvas.from_datastream(ds)
end
it "should encode an image with 16 colors using 4-bit indexed color mode" do
@canvas = ChunkyPNG::Canvas.from_file(png_suite_file("basic", "basn3p04.png"))
ds = ChunkyPNG::Datastream.from_blob(@canvas.to_blob)
expect(ds.header_chunk.color).to eql ChunkyPNG::COLOR_INDEXED
expect(ds.header_chunk.depth).to eql 4
expect(@canvas).to eql ChunkyPNG::Canvas.from_datastream(ds)
end
it "should encode an image with 256 colors using 8-bit indexed color mode" do
@canvas = ChunkyPNG::Canvas.from_file(png_suite_file("basic", "basn3p08.png"))
ds = ChunkyPNG::Datastream.from_blob(@canvas.to_blob)
expect(ds.header_chunk.color).to eql ChunkyPNG::COLOR_INDEXED
expect(ds.header_chunk.depth).to eql 8
expect(@canvas).to eql ChunkyPNG::Canvas.from_datastream(ds)
end
it "should use a higher bit depth than necessary if requested" do
@canvas = ChunkyPNG::Canvas.from_file(png_suite_file("basic", "basn3p01.png"))
ds = ChunkyPNG::Datastream.from_blob(@canvas.to_blob(bit_depth: 4))
expect(ds.header_chunk.color).to eql ChunkyPNG::COLOR_INDEXED
expect(ds.header_chunk.depth).to eql 4
expect(@canvas).to eql ChunkyPNG::Canvas.from_datastream(ds)
end
it "should encode an image with interlacing correctly" do
input_canvas = ChunkyPNG::Canvas.from_file(resource_file("operations.png"))
blob = input_canvas.to_blob(interlace: true)
ds = ChunkyPNG::Datastream.from_blob(blob)
expect(ds.header_chunk.interlace).to eql ChunkyPNG::INTERLACING_ADAM7
expect(ChunkyPNG::Canvas.from_datastream(ds)).to eql input_canvas
end
it "should save an image using the normal routine correctly" do
canvas = reference_canvas("operations")
expect(Zlib::Deflate).to receive(:deflate).with(anything, Zlib::DEFAULT_COMPRESSION).and_return("")
canvas.to_blob
end
it "should save an image using the :fast_rgba routine correctly" do
canvas = reference_canvas("operations")
expect(canvas).to_not receive(:encode_png_str_scanline_none)
expect(canvas).to_not receive(:encode_png_str_scanline_sub)
expect(canvas).to_not receive(:encode_png_str_scanline_up)
expect(canvas).to_not receive(:encode_png_str_scanline_average)
expect(canvas).to_not receive(:encode_png_str_scanline_paeth)
expect(Zlib::Deflate).to receive(:deflate).with(anything, Zlib::BEST_SPEED).and_return("")
canvas.to_blob(:fast_rgba)
end
it "should save an image using the :good_compression routine correctly" do
canvas = reference_canvas("operations")
expect(canvas).to_not receive(:encode_png_str_scanline_none)
expect(canvas).to_not receive(:encode_png_str_scanline_sub)
expect(canvas).to_not receive(:encode_png_str_scanline_up)
expect(canvas).to_not receive(:encode_png_str_scanline_average)
expect(canvas).to_not receive(:encode_png_str_scanline_paeth)
expect(Zlib::Deflate).to receive(:deflate).with(anything, Zlib::BEST_COMPRESSION).and_return("")
canvas.to_blob(:good_compression)
end
it "should save an image using the :best_compression routine correctly" do
canvas = reference_canvas("operations")
expect(canvas).to receive(:encode_png_str_scanline_paeth).exactly(canvas.height).times
expect(Zlib::Deflate).to receive(:deflate).with(anything, Zlib::BEST_COMPRESSION).and_return("")
canvas.to_blob(:best_compression)
end
it "should save an image with black and white only if requested" do
ds = ChunkyPNG::Datastream.from_blob(reference_canvas("lines").to_blob(:black_and_white))
expect(ds.header_chunk.color).to eql ChunkyPNG::COLOR_GRAYSCALE
expect(ds.header_chunk.depth).to eql 1
end
end
describe "different color modes and bit depths" do
before do
@canvas = ChunkyPNG::Canvas.new(2, 2)
# rubocop:disable Layout/ExtraSpacing, Layout/SpaceInsideParens
@canvas[0, 0] = ChunkyPNG::Color.rgba( 1, 2, 3, 4)
@canvas[1, 0] = ChunkyPNG::Color.rgba(252, 253, 254, 255)
@canvas[0, 1] = ChunkyPNG::Color.rgba(255, 254, 253, 252)
@canvas[1, 1] = ChunkyPNG::Color.rgba( 4, 3, 2, 1)
# rubocop:enable Layout/ExtraSpacing, Layout/SpaceInsideParens
@canvas.encoding_palette = @canvas.palette
@canvas.encoding_palette.to_plte_chunk
end
it "should encode using 8-bit RGBA mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_TRUECOLOR_ALPHA, 8, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x01\x02\x03\x04\xFC\xFD\xFE\xFF\0\xFF\xFE\xFD\xFC\x04\x03\x02\x01".force_encoding(Encoding::BINARY)
end
it "should encode using 8 bit RGB mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_TRUECOLOR, 8, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x01\x02\x03\xFC\xFD\xFE\0\xFF\xFE\xFD\x04\x03\x02".force_encoding(Encoding::BINARY)
end
it "should encode using 1-bit grayscale mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_GRAYSCALE, 1, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x40\0\x80".force_encoding(Encoding::BINARY) # Using the B byte of the pixel == 3, assuming R == G == B for grayscale images
end
it "should encode using 2-bit grayscale mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_GRAYSCALE, 2, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x30\0\xC0".force_encoding(Encoding::BINARY) # Using the B byte of the pixel == 3, assuming R == G == B for grayscale images
end
it "should encode using 4-bit grayscale mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_GRAYSCALE, 4, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x0F\0\xF0".force_encoding(Encoding::BINARY) # Using the B byte of the pixel == 3, assuming R == G == B for grayscale images
end
it "should encode using 8-bit grayscale mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_GRAYSCALE, 8, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x03\xFE\0\xFD\x02".force_encoding(Encoding::BINARY) # Using the B byte of the pixel == 3, assuming R == G == B for grayscale images
end
it "should not encode using 1-bit indexed mode because the image has too many colors" do
expect {
@canvas.encode_png_pixelstream(ChunkyPNG::COLOR_INDEXED, 1, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
}.to raise_error(ChunkyPNG::ExpectationFailed)
end
it "should encode using 2-bit indexed mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_INDEXED, 2, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x20\0\xD0".force_encoding(Encoding::BINARY)
end
it "should encode using 4-bit indexed mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_INDEXED, 4, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x02\0\x31".force_encoding(Encoding::BINARY)
end
it "should encode using 8-bit indexed mode correctly" do
stream = @canvas.encode_png_pixelstream(ChunkyPNG::COLOR_INDEXED, 8, ChunkyPNG::INTERLACING_NONE, ChunkyPNG::FILTER_NONE)
expect(stream).to eql "\0\x00\x02\0\x03\x01".force_encoding(Encoding::BINARY)
end
end
describe "different filter methods" do
it "should encode a scanline without filtering correctly" do
stream = [ChunkyPNG::FILTER_NONE, 0, 0, 0, 1, 1, 1, 2, 2, 2].pack("C*")
encode_png_str_scanline_none(stream, 0, nil, 9, 3)
expect(stream.unpack("C*")).to eql [ChunkyPNG::FILTER_NONE, 0, 0, 0, 1, 1, 1, 2, 2, 2]
end
it "should encode a scanline with sub filtering correctly" do
stream = [
ChunkyPNG::FILTER_NONE, 255, 255, 255, 255, 255, 255, 255, 255, 255,
ChunkyPNG::FILTER_NONE, 255, 255, 255, 255, 255, 255, 255, 255, 255,
].pack("C*")
# Check line with previous line
encode_png_str_scanline_sub(stream, 10, 0, 9, 3)
expect(stream.unpack("@10C10")).to eql [ChunkyPNG::FILTER_SUB, 255, 255, 255, 0, 0, 0, 0, 0, 0]
# Check line without previous line
encode_png_str_scanline_sub(stream, 0, nil, 9, 3)
expect(stream.unpack("@0C10")).to eql [ChunkyPNG::FILTER_SUB, 255, 255, 255, 0, 0, 0, 0, 0, 0]
end
it "should encode a scanline with up filtering correctly" do
stream = [
ChunkyPNG::FILTER_NONE, 255, 255, 255, 255, 255, 255, 255, 255, 255,
ChunkyPNG::FILTER_NONE, 255, 255, 255, 255, 255, 255, 255, 255, 255,
].pack("C*")
# Check line with previous line
encode_png_str_scanline_up(stream, 10, 0, 9, 3)
expect(stream.unpack("@10C10")).to eql [ChunkyPNG::FILTER_UP, 0, 0, 0, 0, 0, 0, 0, 0, 0]
# Check line without previous line
encode_png_str_scanline_up(stream, 0, nil, 9, 3)
expect(stream.unpack("@0C10")).to eql [ChunkyPNG::FILTER_UP, 255, 255, 255, 255, 255, 255, 255, 255, 255]
end
it "should encode a scanline with average filtering correctly" do
stream = [
ChunkyPNG::FILTER_NONE, 10, 20, 30, 40, 50, 60, 70, 80, 80, 100, 110, 120, # rubocop:disable Layout/ExtraSpacing
ChunkyPNG::FILTER_NONE, 5, 10, 25, 45, 45, 55, 80, 125, 105, 150, 114, 165, # rubocop:disable Layout/ExtraSpacing
].pack("C*")
# Check line with previous line
encode_png_str_scanline_average(stream, 13, 0, 12, 3)
expect(stream.unpack("@13C13")).to eql [ChunkyPNG::FILTER_AVERAGE, 0, 0, 10, 23, 15, 13, 23, 63, 38, 60, 253, 53]
# Check line without previous line
encode_png_str_scanline_average(stream, 0, nil, 12, 3)
expect(stream.unpack("@0C13")).to eql [ChunkyPNG::FILTER_AVERAGE, 10, 20, 30, 35, 40, 45, 50, 55, 50, 65, 70, 80]
end
it "should encode a scanline with paeth filtering correctly" do
stream = [
ChunkyPNG::FILTER_NONE, 10, 20, 30, 40, 50, 60, 70, 80, 80, 100, 110, 120, # rubocop:disable Layout/ExtraSpacing
ChunkyPNG::FILTER_NONE, 10, 20, 40, 60, 60, 60, 70, 120, 90, 120, 54, 120, # rubocop:disable Layout/ExtraSpacing
].pack("C*")
# Check line with previous line
encode_png_str_scanline_paeth(stream, 13, 0, 12, 3)
expect(stream.unpack("@13C13")).to eql [ChunkyPNG::FILTER_PAETH, 0, 0, 10, 20, 10, 0, 0, 40, 10, 20, 190, 0]
# Check line without previous line
encode_png_str_scanline_paeth(stream, 0, nil, 12, 3)
expect(stream.unpack("@0C13")).to eql [ChunkyPNG::FILTER_PAETH, 10, 20, 30, 30, 30, 30, 30, 30, 20, 30, 30, 40]
end
end
end
chunky_png-1.3.15/spec/chunky_png/vector_spec.rb 0000644 0001750 0001750 00000006536 13766004353 021201 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Vector do
subject { ChunkyPNG::Vector.new([ChunkyPNG::Point.new(2, 5), ChunkyPNG::Point.new(1, 3), ChunkyPNG::Point.new(4, 6)]) }
it { should respond_to(:points) }
describe "#length" do
it "shopuld have 3 items" do
expect(subject.length).to eql(3)
end
end
describe "#x_range" do
it "should get the right range of x values" do
expect(subject.x_range).to eql(1..4)
end
it "should find the minimum x-coordinate" do
expect(subject.min_x).to eql(1)
end
it "should find the maximum x-coordinate" do
expect(subject.max_x).to eql(4)
end
it "should calculate the width correctly" do
expect(subject.width).to eql(4)
end
end
describe "#y_range" do
it "should get the right range of y values" do
expect(subject.y_range).to eql(3..6)
end
it "should find the minimum x-coordinate" do
expect(subject.min_y).to eql(3)
end
it "should find the maximum x-coordinate" do
expect(subject.max_y).to eql(6)
end
it "should calculate the height correctly" do
expect(subject.height).to eql(4)
end
end
describe "#offset" do
it "should return a ChunkyPNG::Point" do
expect(subject.offset).to be_kind_of(ChunkyPNG::Point)
end
it "should use the mininum x and y coordinates as values for the point" do
expect(subject.offset.x).to eql subject.min_x
expect(subject.offset.y).to eql subject.min_y
end
end
describe "#dimension" do
it "should return a ChunkyPNG::Dimension" do
expect(subject.dimension).to be_kind_of(ChunkyPNG::Dimension)
end
it "should use the width and height of the vector for the dimension" do
expect(subject.dimension.width).to eql subject.width
expect(subject.dimension.height).to eql subject.height
end
end
describe "#edges" do
it "should get three edges when closing the path" do
expect(subject.edges(true).to_a).to eql [
[ChunkyPNG::Point.new(2, 5), ChunkyPNG::Point.new(1, 3)],
[ChunkyPNG::Point.new(1, 3), ChunkyPNG::Point.new(4, 6)],
[ChunkyPNG::Point.new(4, 6), ChunkyPNG::Point.new(2, 5)],
]
end
it "should get two edges when not closing the path" do
expect(subject.edges(false).to_a).to eql [
[ChunkyPNG::Point.new(2, 5), ChunkyPNG::Point.new(1, 3)],
[ChunkyPNG::Point.new(1, 3), ChunkyPNG::Point.new(4, 6)],
]
end
end
end
describe "ChunkyPNG.Vector" do
let(:example) { ChunkyPNG::Vector.new([ChunkyPNG::Point.new(2, 4), ChunkyPNG::Point.new(1, 2), ChunkyPNG::Point.new(3, 6)]) }
it "should return an empty vector when given an empty array" do
expect(ChunkyPNG::Vector()).to eql ChunkyPNG::Vector.new([])
expect(ChunkyPNG::Vector(*[])).to eql ChunkyPNG::Vector.new([]) # rubocop:disable Lint/UnneededSplatExpansion
end
it "should raise an error when an odd number of numerics is given" do
expect { ChunkyPNG::Vector(1, 2, 3) }.to raise_error(ArgumentError)
end
it "should create a vector from a string" do
expect(ChunkyPNG::Vector("(2,4) (1,2) (3,6)")).to eql example
end
it "should create a vector from a flat array" do
expect(ChunkyPNG::Vector(2, 4, 1, 2, 3, 6)).to eql example
end
it "should create a vector from a nested array" do
expect(ChunkyPNG::Vector("(2,4)", [1, 2], x: 3, y: 6)).to eql example
end
end
chunky_png-1.3.15/spec/chunky_png/image_spec.rb 0000644 0001750 0001750 00000001724 13766004353 020753 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Image do
describe "#metadata" do
it "should load metadata from an existing file" do
image = ChunkyPNG::Image.from_file(resource_file("text_chunk.png"))
expect(image.metadata["Title"]).to eql "My amazing icon!"
expect(image.metadata["Author"]).to eql "Willem van Bergen"
end
it "should write metadata to the file correctly" do
filename = resource_file("_metadata.png")
image = ChunkyPNG::Image.new(10, 10)
image.metadata["Title"] = "My amazing icon!"
image.metadata["Author"] = "Willem van Bergen"
image.save(filename)
metadata = ChunkyPNG::Datastream.from_file(filename).metadata
expect(metadata["Title"]).to eql "My amazing icon!"
expect(metadata["Author"]).to eql "Willem van Bergen"
end
it "should load empty images correctly" do
expect { ChunkyPNG::Image.from_file(resource_file("empty.png")) }.to_not raise_error
end
end
end
chunky_png-1.3.15/spec/chunky_png/rmagick_spec.rb 0000644 0001750 0001750 00000001215 13766004353 021301 0 ustar daniel daniel require "spec_helper"
begin
require "chunky_png/rmagick"
describe ChunkyPNG::RMagick do
it "should import an image from RMagick correctly" do
image = Magick::Image.read(resource_file("composited.png")).first
canvas = ChunkyPNG::RMagick.import(image)
expect(canvas).to eql reference_canvas("composited")
end
it "should export an image to RMagick correctly" do
canvas = reference_canvas("composited")
image = ChunkyPNG::RMagick.export(canvas)
image.format = "PNG32"
expect(canvas).to eql ChunkyPNG::Canvas.from_blob(image.to_blob)
end
end
rescue LoadError
# skipping RMagick tests
end
chunky_png-1.3.15/spec/chunky_png/dimension_spec.rb 0000644 0001750 0001750 00000002775 13766004353 021665 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG::Dimension do
subject { ChunkyPNG::Dimension.new(2, 3) }
it { should respond_to(:width) }
it { should respond_to(:height) }
describe "#area" do
it "should calculate the area correctly" do
expect(subject.area).to eql 6
end
end
end
describe "ChunkyPNG.Dimension" do
subject { ChunkyPNG::Dimension.new(1, 2) }
it "should create a dimension from a 2-item array" do
expect(ChunkyPNG::Dimension([1, 2])).to eql subject
expect(ChunkyPNG::Dimension(["1", "2"])).to eql subject
end
it "should create a dimension from a hash with width and height keys" do
expect(ChunkyPNG::Dimension(width: 1, height: 2)).to eql subject
expect(ChunkyPNG::Dimension("width" => "1", "height" => "2")).to eql subject
end
it "should create a dimension from a point-like string" do
[
ChunkyPNG::Dimension("1,2"),
ChunkyPNG::Dimension("1 2"),
ChunkyPNG::Dimension("(1 , 2)"),
ChunkyPNG::Dimension("{1x2}"),
ChunkyPNG::Dimension("[1\t2}"),
].all? { |point| point == subject }
end
it "should create a dimension from an object that responds to width and height" do
mock_object = Struct.new(:width, :height).new(1, 2)
expect(ChunkyPNG::Dimension(mock_object)).to eql subject
end
it "should raise an exception if the input is not understood" do
expect { ChunkyPNG::Dimension(Object.new) }.to raise_error(ArgumentError)
expect { ChunkyPNG::Dimension(1, 2, 3) }.to raise_error(ArgumentError)
end
end
chunky_png-1.3.15/spec/chunky_png_spec.rb 0000644 0001750 0001750 00000000235 13766004353 017665 0 ustar daniel daniel require "spec_helper"
describe ChunkyPNG do
it "should have a VERSION constant" do
expect(ChunkyPNG.const_defined?("VERSION")).to be_truthy
end
end
chunky_png-1.3.15/spec/spec_helper.rb 0000644 0001750 0001750 00000002453 13766004353 017003 0 ustar daniel daniel require "rubygems"
require "bundler/setup"
require "chunky_png"
module PNGSuite
def png_suite_file(kind, file)
File.join(png_suite_dir(kind), file)
end
def png_suite_dir(kind)
File.expand_path("./png_suite/#{kind}", File.dirname(__FILE__))
end
def png_suite_files(kind, pattern = "*.png")
Dir[File.join(png_suite_dir(kind), pattern)]
end
end
module ResourceFileHelper
def resource_file(name)
File.expand_path("./resources/#{name}", File.dirname(__FILE__))
end
def resource_data(name)
data = nil
File.open(resource_file(name), "rb") { |f| data = f.read }
data
end
def reference_canvas(name)
ChunkyPNG::Canvas.from_file(resource_file("#{name}.png"))
end
def reference_image(name)
ChunkyPNG::Image.from_file(resource_file("#{name}.png"))
end
def display(png)
filename = resource_file("_tmp.png")
png.save(filename)
`open #{filename}`
end
end
module ChunkOperationsHelper
def serialized_chunk(chunk)
chunk.write(stream = StringIO.new)
stream.rewind
ChunkyPNG::Chunk.read(stream)
end
end
RSpec.configure do |config|
config.extend PNGSuite
config.include PNGSuite
config.include ResourceFileHelper
config.include ChunkOperationsHelper
config.expect_with :rspec do |c|
c.syntax = [:should, :expect]
end
end
chunky_png-1.3.15/spec/png_suite_spec.rb 0000644 0001750 0001750 00000012456 13766004353 017525 0 ustar daniel daniel require "spec_helper"
describe "PNG testuite" do
context "Decoding broken images" do
png_suite_files(:broken).each do |file|
it "should report #{File.basename(file)} as broken" do
expect { ChunkyPNG::Image.from_file(file) }.to raise_error(ChunkyPNG::Exception)
end
end
end
context "Decoding supported images" do
png_suite_files(:basic, "*.png").each do |file|
reference = file.sub(/\.png$/, ".rgba")
color_mode = file.match(/[in](\d)[apgc](\d\d)\.png$/)[1].to_i
bit_depth = file.match(/[in](\d)[apgc](\d\d)\.png$/)[2].to_i
it "should decode #{File.basename(file)} (color mode: #{color_mode}, bit depth: #{bit_depth}) exactly the same as the reference image" do
decoded = ChunkyPNG::Canvas.from_file(file)
expect(decoded.to_rgba_stream).to eql(File.read(reference, mode: "rb"))
end
end
end
context "Decoding text chunks" do
it "should not find metadata in a file without text chunks" do
image = ChunkyPNG::Image.from_file(png_suite_file(:metadata, "cm0n0g04.png"))
expect(image.metadata).to be_empty
end
# it "should find metadata in a file with uncompressed text chunks" do
# image = ChunkyPNG::Image.from_file(png_suite_file(:metadata, 'cm7n0g04.png'))
# image.metadata.should_not be_empty
# end
#
# it "should find metadata in a file with compressed text chunks" do
# image = ChunkyPNG::Image.from_file(png_suite_file(:metadata, 'cm9n0g04.png'))
# image.metadata.should_not be_empty
# end
end
context "Decoding filter methods" do
png_suite_files(:filtering, "*_reference.png").each do |reference_file|
file = reference_file.sub(/_reference\.png$/, ".png")
filter_method = file.match(/f(\d\d)[a-z0-9]+\.png/)[1].to_i
it "should decode #{File.basename(file)} (filter method: #{filter_method}) exactly the same as the reference image" do
decoded = ChunkyPNG::Canvas.from_file(file)
reference = ChunkyPNG::Canvas.from_file(reference_file)
expect(decoded).to eql reference
end
end
end
context "Decoding different chunk splits" do
it "should decode grayscale images successfully regardless of the data chunk ordering and splitting" do
reference = ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi1n0g16.png")).imagedata
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi2n0g16.png")).imagedata).to eql reference
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi4n0g16.png")).imagedata).to eql reference
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi9n0g16.png")).imagedata).to eql reference
end
it "should decode color images successfully regardless of the data chunk ordering and splitting" do
reference = ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi1n2c16.png")).imagedata
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi2n2c16.png")).imagedata).to eql reference
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi4n2c16.png")).imagedata).to eql reference
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:chunk_ordering, "oi9n2c16.png")).imagedata).to eql reference
end
end
context "Decoding different compression levels" do
it "should decode the image successfully regardless of the compression level" do
reference = ChunkyPNG::Datastream.from_file(png_suite_file(:compression_levels, "z00n2c08.png")).imagedata
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:compression_levels, "z03n2c08.png")).imagedata).to eql reference
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:compression_levels, "z06n2c08.png")).imagedata).to eql reference
expect(ChunkyPNG::Datastream.from_file(png_suite_file(:compression_levels, "z09n2c08.png")).imagedata).to eql reference
end
end
context "Decoding transparency" do
png_suite_files(:transparency, "tp0*.png").each do |file|
it "should not have transparency in #{File.basename(file)}" do
expect(ChunkyPNG::Color.a(ChunkyPNG::Image.from_file(file)[0, 0])).to eql 255
end
end
png_suite_files(:transparency, "tp1*.png").each do |file|
it "should have transparency in #{File.basename(file)}" do
expect(ChunkyPNG::Color.a(ChunkyPNG::Image.from_file(file)[0, 0])).to eql 0
end
end
png_suite_files(:transparency, "tb*.png").each do |file|
it "should have transparency in #{File.basename(file)}" do
expect(ChunkyPNG::Color.a(ChunkyPNG::Image.from_file(file)[0, 0])).to eql 0
end
end
end
context "Decoding different sizes" do
png_suite_files(:sizes, "*n*.png").each do |file|
dimension = file.match(/s(\d\d)n\dp\d\d/)[1].to_i
it "should create a canvas with a #{dimension}x#{dimension} size" do
canvas = ChunkyPNG::Image.from_file(file)
expect(canvas.width).to eql dimension
expect(canvas.height).to eql dimension
end
it "should decode the #{dimension}x#{dimension} interlaced image exactly the same the non-interlaced version" do
interlaced_file = file.sub(/n3p(\d\d)\.png$/, 'i3p\\1.png')
expect(ChunkyPNG::Image.from_file(interlaced_file)).to eql ChunkyPNG::Image.from_file(file)
end
end
end
end
chunky_png-1.3.15/spec/resources/ 0000755 0001750 0001750 00000000000 13766004353 016173 5 ustar daniel daniel chunky_png-1.3.15/spec/resources/bezier_six_point.png 0000644 0001750 0001750 00000000204 13766004353 022251 0 ustar daniel daniel PNG
IHDR U KIDATx-ʱ
0CQb F`F8(tt(%R