module ChunkyPNG
# 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 Fixnum.
#
# Pixels/colors are represented in RGBA componetns. 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
# Fixnum.
#
# 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
# The maximum value of each color component.
MAX = 0xff
####################################################################
# CONSTRUCTING COLOR VALUES
####################################################################
# Creates a new color using an r, g, b triple and an alpha value.
# @return [Fixnum] 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.
# @return [Fixnum] 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.
# @return [ChunkyPNG::Color] 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.
# @return [Fixnum] 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 [Fixnum] pos The position in the string to load the triple from.
# @return [Fixnum] 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 [Fixnum] pos The position in the string to load the triple from.
# @return [Fixnum] 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.
# Color strings may include the prefix "0x" or "#".
#
# @param [String] str The color in hex notation. @return [Fixnum] The
# converted color value.
def from_hex(str)
case str
when /^(?:#|0x)?([0-9a-f]{6})$/i then ($1.hex << 8) | 0xff
when /^(?:#|0x)?([0-9a-f]{8})$/i then $1.hex
else raise "Not a valid hex color notation: #{str.inspect}!"
end
end
####################################################################
# PROPERTIES
####################################################################
# Returns the red-component from the color value.
#
# @param [Fixnum] value The color value.
# @return [Fixnum] A value between 0 and MAX.
def r(value)
(value & 0xff000000) >> 24
end
# Returns the green-component from the color value.
#
# @param [Fixnum] value The color value.
# @return [Fixnum] A value between 0 and MAX.
def g(value)
(value & 0x00ff0000) >> 16
end
# Returns the blue-component from the color value.
#
# @param [Fixnum] value The color value.
# @return [Fixnum] A value between 0 and MAX.
def b(value)
(value & 0x0000ff00) >> 8
end
# Returns the alpha channel value for the color value.
#
# @param [Fixnum] value The color value.
# @return [Fixnum] A value between 0 and MAX.
def a(value)
value & 0x000000ff
end
# Returns true if this color is fully opaque.
#
# @param [Fixnum] 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 [Fixnum] value The color to transform.
# @return [Fixnum] The opauq color
def opaque!(value)
value | 0x000000ff
end
# Returns true if this color is fully transparent.
#
# @param [Fixnum] 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 [Fixnum] 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 [Fixnum] a The first fraction.
# @param [Fixnum] b The second fraction.
# @return [Fixnum] 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 [Fixnum] fg The foreground color.
# @param [Fixnum] bg The foreground color.
# @return [Fixnum] The composited color.
# @see ChunkyPNG::Color#compose_precise
def compose_quick(fg, bg)
return fg if opaque?(fg)
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 [Fixnum] fg The foreground color.
# @param [Fixnum] bg The foreground color.
# @return [Fixnum] The composited color.
# @see ChunkyPNG::Color#compose_quick
def compose_precise(fg, bg)
return fg if opaque?(fg)
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 [Fixnum] fg The foreground color.
# @param [Fixnum] bg The foreground color.
# @return [Fixnum] The blended color.
def blend(fg, bg)
(fg + bg) >> 1
end
# Lowers the intensity of a color, by lowering its alpha by a given factor.
# @param [Fixnum] color The color to adjust.
# @param [Fixnum] factor Fade factor as an integer between 0 and 255.
# @return [Fixnum] 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
# transparentvariant of the mask color.
#
# @param [Fixnum] color The color that was the result of compositing.
# @param [Fixnum] mask The opaque variant of the color that was being composed
# @param [Fixnum] bg The background color on which the color was composed.
# @param [Fixnum] tolerance The decomposition tolerance level, a value between 0 and 255.
# @return [Fixnum] 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 [Fixnum] color The color that was the result of compositing.
# @param [Fixnum] mask The opauqe variant of the color that was being composed
# @param [Fixnum] bg The background color on which the color was composed.
# @param [Fixnum] tolerance The decomposition tolerance level, a value between 0 and 255.
# @return [Fixnum] The decomposed alpha channel value, between 0 and 255.
# @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
return 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 [Fixnum] color The color that was the result of compositing.
# @param [Fixnum] mask The opauqe variant of the color that was being composed
# @param [Fixnum] bg The background color on which the color was composed.
# @return [Fixnum] 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] The channel to decompose the alpha channel from.
# @param [Fixnum] color The color that was the result of compositing.
# @param [Fixnum] mask The opauqe variant of the color that was being composed
# @param [Fixnum] bg The background color on which the color was composed.
# @param [Fixnum] The decomposed alpha value for the channel.
def decompose_alpha_component(channel, color, mask, bg)
((send(channel, bg) - send(channel, color)).to_f /
(send(channel, bg) - send(channel, mask)).to_f * MAX).round
end
# Decomposes the alpha channels for the r, g and b color channel.
# @param [Fixnum] color The color that was the result of compositing.
# @param [Fixnum] mask The opauqe variant of the color that was being composed
# @param [Fixnum] bg The background color on which the color was composed.
# @return [Array<Fixnum>] 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 [Fixnum] value The color to convert.
# @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 RGBA values for this color.
#
# @param [Fixnum] color The color to convert.
# @return [Array<Fixnum>] An array with 4 Fixnum 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 [Fixnum] color The color to convert.
# @return [Array<Fixnum>] An array with 3 Fixnum 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 [Fixnum] color The grayscale color to convert.
# @return [Array<Fixnum>] An array with 1 Fixnum element.
def to_grayscale_bytes(color)
[r(color)] # assumption r == g == b
end
# Returns an array with the grayscale teint and alpha channel values
# for this color.
#
# This method expects the r,g and b value to be equal.
#
# @param [Fixnum] color The grayscale color to convert.
# @return [Array<Fixnum>] An array with 2 Fixnum elements.
def to_grayscale_alpha_bytes(color)
[r(color), a(color)] # assumption r == g == b
end
####################################################################
# COLOR CONSTANTS
####################################################################
# Black pixel/color
BLACK = rgb( 0, 0, 0)
# White pixel/color
WHITE = rgb(255, 255, 255)
# Fully transparent pixel/color
TRANSPARENT = rgba(255, 255, 255, 0)
####################################################################
# STATIC UTILITY METHODS
####################################################################
# Returns the size in bytes of a pixel when it is stored using a given color mode.
# @param [Fixnum] color_mode The color mode in which the pixels are stored.
# @return [Fixnum] The number of bytes used per pixel in a datastream.
def bytesize(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 "Don't know the bytesize of pixels in this colormode: #{color_mode}!"
end
end
end
end