color-2.2.0/0000755000004100000410000000000015137215736012673 5ustar www-datawww-datacolor-2.2.0/Manifest.txt0000644000004100000410000000074415137215736015207 0ustar www-datawww-dataCHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md CONTRIBUTORS.md LICENCE.md Manifest.txt README.md Rakefile SECURITY.md lib/color.rb lib/color/cielab.rb lib/color/cmyk.rb lib/color/grayscale.rb lib/color/hsl.rb lib/color/rgb.rb lib/color/rgb/colors.rb lib/color/version.rb lib/color/xyz.rb lib/color/yiq.rb licences/dco.txt test/fixtures/cielab.json test/minitest_helper.rb test/test_cmyk.rb test/test_color.rb test/test_grayscale.rb test/test_hsl.rb test/test_rgb.rb test/test_yiq.rb color-2.2.0/color.gemspec0000644000004100000410000001124115137215736015355 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: color 2.2.0 ruby lib Gem::Specification.new do |s| s.name = "color".freeze s.version = "2.2.0".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.metadata = { "bug_tracker_uri" => "https://github.com/halostatue/color/issues", "changelog_uri" => "https://github.com/halostatue/color/blob/main/CHANGELOG.md", "documentation_uri" => "https://halostatue.github.io/color/", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/halostatue/color" } if s.respond_to? :metadata= s.require_paths = ["lib".freeze] s.authors = ["Austin Ziegler".freeze, "Matt Lyon".freeze] s.date = "1970-01-01" s.description = "Color is a Ruby library to provide RGB, CMYK, HSL, and other color space\nmanipulation support to applications that require it. It provides optional named\nRGB colors that are commonly supported in HTML, SVG, and X11 applications.\n\nThe Color library performs purely mathematical manipulation of the colors based\non color theory without reference to device color profiles (such as sRGB or\nAdobe RGB). For most purposes, when working with RGB and HSL color spaces, this\nwon't matter. Absolute color spaces (like CIE LAB and CIE XYZ) cannot be\nreliably converted to relative color spaces (like RGB) without color profiles.\nWhen necessary for conversions, Color provides D65 and D50 reference white\nvalues in Color::XYZ.\n\nColor 2.2 adds a minor feature where an RGB color created from values can\nsilently inherit the `#name` of a predefined color if `color/rgb/colors` has\nalready been loaded. It builds on the Color 2.0 major release, dropping support\nfor all versions of Ruby prior to 3.2 as well as removing or renaming a number\nof features. The main breaking changes are:\n\n- Color classes are immutable Data objects; they are no longer mutable.\n- RGB named colors are no longer loaded on gem startup, but must be required\n explicitly (this is _not_ done via `autoload` because there are more than 100\n named colors with spelling variations) with `require \"color/rgb/colors\"`.\n- Color palettes have been removed.\n- `Color::CSS` and `Color::CSS#[]` have been removed.".freeze s.email = ["halostatue@gmail.com".freeze, "matt@postsomnia.com".freeze] s.extra_rdoc_files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "SECURITY.md".freeze, "licences/dco.txt".freeze] s.files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "Rakefile".freeze, "SECURITY.md".freeze, "lib/color.rb".freeze, "lib/color/cielab.rb".freeze, "lib/color/cmyk.rb".freeze, "lib/color/grayscale.rb".freeze, "lib/color/hsl.rb".freeze, "lib/color/rgb.rb".freeze, "lib/color/rgb/colors.rb".freeze, "lib/color/version.rb".freeze, "lib/color/xyz.rb".freeze, "lib/color/yiq.rb".freeze, "licences/dco.txt".freeze, "test/fixtures/cielab.json".freeze, "test/minitest_helper.rb".freeze, "test/test_cmyk.rb".freeze, "test/test_color.rb".freeze, "test/test_grayscale.rb".freeze, "test/test_hsl.rb".freeze, "test/test_rgb.rb".freeze, "test/test_yiq.rb".freeze] s.homepage = "https://github.com/halostatue/color".freeze s.licenses = ["MIT".freeze] s.rdoc_options = ["--main".freeze, "README.md".freeze] s.required_ruby_version = Gem::Requirement.new(">= 3.2".freeze) s.rubygems_version = "4.0.4".freeze s.summary = "Color is a Ruby library to provide RGB, CMYK, HSL, and other color space manipulation support to applications that require it".freeze s.specification_version = 4 s.add_development_dependency(%q.freeze, ["~> 4.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 3.0".freeze]) s.add_development_dependency(%q.freeze, [">= 0.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 6.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.1".freeze]) s.add_development_dependency(%q.freeze, [">= 10.0".freeze, "< 14".freeze]) s.add_development_dependency(%q.freeze, [">= 6.0".freeze, "< 8".freeze]) s.add_development_dependency(%q.freeze, ["~> 0.22".freeze]) s.add_development_dependency(%q.freeze, ["~> 0.8".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.50".freeze]) end color-2.2.0/CONTRIBUTING.md0000644000004100000410000001233215137215736015125 0ustar www-datawww-data# Contributing Contribution to Color is encouraged: bug reports, feature requests, or code contributions. New features should be proposed and discussed in an [issue][issues]. Before contributing patches, please read the [Licence](./LICENCE.md). Color is governed under the [Contributor Covenant Code of Conduct][cccoc]. ## Code Guidelines I have several guidelines to contributing code through pull requests: - All code changes require tests. In most cases, this will be added or updated unit tests. I use [Minitest][minitest]. - I use code formatters, static analysis tools, and linting to ensure consistent styles and formatting. There should be no warning output from test run processes. I use [Standard Ruby][standardrb]. - Proposed changes should be on a thoughtfully-named topic branch and organized into logical commit chunks as appropriate. - Use [Conventional Commits][conventional] with my [conventions](#commit-conventions). - Versions must not be updated in pull requests unless otherwise directed. This means that you must not: - Modify `VERSION` in `lib/color/version.rb`. When your patch is accepted and a release is made, the version will be updated at that point. - Modify `color.gemspec`; it is a generated file. (You _may_ use `rake gemspec` to regenerate it if your change involves metadata related to gem itself). - Modify the `Gemfile`. - Documentation should be added or updated as appropriate for new or updated functionality. The documentation is RDoc; color does not use extensions that may be present in alternative documentation generators. - All GitHub Actions checks marked as required must pass before a pull request may be accepted and merged. - Add your name or GitHub handle to `CONTRIBUTORS.md` and a record in the `CHANGELOG.md` as a separate commit from your main change. (Follow the style in the `CHANGELOG.md` and provide a link to your PR.) - Include your DCO sign-off in each commit message (see [LICENCE](LICENCE.md)). ## AI Contribution Policy Color is a library full of complex math and subtle decisions (some of them possibly even wrong). It is extremely important that contributions of any sort be well understood by the submitter and that the developer can attest to the [Developer Certificate of Origin][dco] for each pull request (see [LICENCE](LICENCE.md)). Any contribution (bug, feature request, or pull request) that uses undeclared AI output will be rejected. ## Test Dependencies Color uses Ryan Davis's [Hoe][Hoe] to manage the release process, and it adds a number of rake tasks. You will mostly be interested in `rake`, which runs the tests the same way that `rake test` will do. To assist with the installation of the development dependencies for color, I have provided the simplest possible Gemfile pointing to the (generated) `color.gemspec` file. This will permit you to do `bundle install` to get the development dependencies. You can run tests with code coverage analysis by running `rake coverage`. ## Commit Conventions Color has adopted a variation of the Conventional Commits format for commit messages. The following types are permitted: | Type | Purpose | | ------- | ----------------------------------------------------- | | `feat` | A new feature | | `fix` | A bug fix | | `chore` | A code change that is neither a bug fix nor a feature | | `docs` | Documentation updates | | `deps` | Dependency updates, including GitHub Actions. | I encourage the use of [Tim Pope's][tpope-qcm] or [Chris Beam's][cbeams] guidelines on the writing of commit messages I require the use of [git][trailers1] [trailers][trailers2] for specific additional metadata and strongly encourage it for others. The conditionally required metadata trailers are: - `Breaking-Change`: if the change is a breaking change. **Do not** use the shorthand form (`feat!(scope)`) or `BREAKING CHANGE`. - `Signed-off-by`: this is required for all developers except me, as outlined in the [Licence](./LICENCE.md#developer-certificate-of-origin). - `Fixes` or `Resolves`: If a change fixes one or more open [issues][issues], that issue must be included in the `Fixes` or `Resolves` trailer. Multiple issues should be listed comma separated in the same trailer: `Fixes: #1, #5, #7`, but _may_ appear in separate trailers. While both `Fixes` and `Resolves` are synonyms, only _one_ should be used in a given commit or pull request. - `Related to`: If a change does not fix an issue, those issue references should be included in this trailer. [cbeams]: https://cbea.ms/git-commit/ [cccoc]: ./CODE_OF_CONDUCT.md [conventional]: https://www.conventionalcommits.org/en/v1.0.0/ [dco]: licences/dco.txt [hoe]: https://github.com/seattlerb/hoe [issues]: https://github.com/halostatue/color/issues [minitest]: https://github.com/seattlerb/minitest [standardrb]: https://github.com/standardrb/standard [tpope-qcm]: https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [trailers1]: https://git-scm.com/docs/git-interpret-trailers [trailers2]: https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---trailerlttokengtltvaluegt color-2.2.0/SECURITY.md0000644000004100000410000000213715137215736014467 0ustar www-datawww-data# color Security Policy ## LLM-Generated Security Report Policy Absolutely no security reports will be accepted that have been generated by LLM agents. ## Supported Versions Security reports are accepted for the most recent major release and the previous version for a limited time after the initial major release version. After a major release, the previous version will receive full support for three months and security support for an additional three months (for a total of six months). Because color 1.x supports a wide range of Ruby versions that are themselves end of life, security reports will only be accepted when they can be demonstrated on Ruby 3.2 or higher. > | Version | Release Date | Support Ends | Security Support Ends | > | ------- | ------------ | ------------ | --------------------- | > | 1.x | 2015-10-26 | 2025-11-07 | 2026-02-07 | > | 2.x | 2025-08-07 | - | - | ## Reporting a Vulnerability Create a [private vulnerability report][advisory] with GitHub. [advisory]: https://github.com/halostatue/color/security/advisories/new color-2.2.0/lib/0000755000004100000410000000000015137215736013441 5ustar www-datawww-datacolor-2.2.0/lib/color.rb0000644000004100000410000001773415137215736015120 0ustar www-datawww-data# frozen_string_literal: true # # \Color -- \Color Math in Ruby # # - **code**: [github.com/halostatue/color](https://github.com/halostatue/color) # - **issues**: [github.com/halostatue/color/issues](https://github.com/halostatue/color/issues) # - **changelog**: [CHANGELOG](rdoc-ref:CHANGELOG.md) # # \Color is a Ruby library to provide RGB, CMYK, HSL, and other color space manipulation # support to applications that require it. It provides optional named RGB colors that are # commonly supported in HTML, SVG, and X11 applications. # # The \Color library performs purely mathematical manipulation of the colors based on # color theory without reference to device color profiles (such as sRGB or Adobe RGB). For # most purposes, when working with RGB and HSL color spaces, this won't matter. Absolute # color spaces (like CIE LAB and CIE XYZ) cannot be reliably converted to relative color # spaces (like RGB) without color profiles. When necessary for conversions, \Color # provides \D65 and \D50 reference white values in Color::XYZ. # # Color 2.1 fixes a Color::XYZ bug where the values were improperly clamped and adds more # Color::XYZ white points for standard illuminants. It builds on the Color 2.0 major # release, dropping support for all versions of Ruby prior to 3.2 as well as removing or # renaming a number of features. The main breaking changes are: # # - Color classes are immutable Data objects; they are no longer mutable. # - RGB named colors are no longer loaded on gem startup, but must be required explicitly # (this is _not_ done via `autoload` because there are more than 100 named colors with # spelling variations) with `require "color/rgb/colors"`. # - Color palettes have been removed. # - `Color::CSS` and `Color::CSS#[]` have been removed. module Color ## # The maximum "resolution" for color math; if any value is less than or equal to this # value, it is treated as zero. EPSILON = 1e-5 ## # The tolerance for comparing the components of two colors. In general, colors are # considered equal if all of their components are within this tolerance value of each # other. TOLERANCE = 1e-4 # :stopdoc: CIELAB = Data.define(:l, :a, :b) CMYK = Data.define(:c, :m, :y, :k) Grayscale = Data.define(:g) HSL = Data.define(:h, :s, :l) RGB = Data.define(:r, :g, :b, :names) XYZ = Data.define(:x, :y, :z) YIQ = Data.define(:y, :i, :q) # :startdoc: ## # It is useful to know the number of components in some cases. Since most colors are # defined with three components, we define a constant value here. Color classes that # require more or less should override this. # # We _could_ define this as `members.count`, but this would require a special case # for Color::RGB _regardless_ because there's an additional member for RGB colors # (names). def components = 3 # :nodoc: ## # Compares the `other` color to this one. The `other` color will be coerced to the same # type as the current color. Such converted color comparisons will always be more # approximate than non-converted comparisons. # # All values are compared as floating-point values, so two colors will be reported # equivalent if all component values are within +TOLERANCE+ of each other. def ==(other) other.is_a?(Color) && to_internal.zip(coerce(other).to_internal).all? { near?(_1, _2) } end ## # Apply the provided block to each color component in turn, returning a new color # instance. def map(&block) = self.class.from_internal(*to_internal.map(&block)) ## # Apply the provided block to the color component pairs in turn, returning a new color # instance. def map_with(other, &block) = self.class.from_internal(*zip(other).map(&block)) ## # Zip the color component pairs together. def zip(other) = to_internal.zip(coerce(other).to_internal) ## # Multiplies each component value by the scaling factor or factors, returning a new # color object with the scaled values. # # If a single scaling factor is provided, it is applied to all components: # # ```ruby # rgb = Color::RGB::Wheat # => RGB [#f5deb3] # rgb.scale(0.75) # => RGB [#b8a786] # ``` # # If more than one scaling factor is provided, there must be exactly one factor for each # color component of the color object or an `ArgumentError` will be raised. # # ```ruby # rgb = Color::RGB::Wheat # => RGB [#f5deb3] # # 0xf5 * 0 == 0x00, 0xde * 0.5 == 0x6f, 0xb3 * 2 == 0x166 (clamped to 0xff) # rgb.scale(0, 0.5, 2) # => RGB [#006fff] # # rgb.scale(1, 2) # => Invalid scaling factors [1, 2] for Color::RGB (ArgumentError) # ``` def scale(*factors) if factors.size == 1 factor = factors.first map { _1 * factor } elsif factors.size != components raise ArgumentError, "Invalid scaling factors #{factors.inspect} for #{self.class}" else new_components = to_internal.zip(factors).map { _1 * _2 } self.class.from_internal(*new_components) end end ## def css_value(value, format = nil) # :nodoc: if value.nil? "none" elsif near_zero?(value) "0" else suffix = case format in :percent "%" in :degrees "deg" else "" end "%3.2f%s" % [value, suffix] end end private ## def from_internal(...) = self.class.from_internal(...) ## # Returns `true` if the value is less than EPSILON. def near_zero?(value) = (value.abs <= Color::EPSILON) # :nodoc: ## # Returns `true` if the value is within EPSILON of zero or less than zero. def near_zero_or_less?(value) = (value < 0.0 or near_zero?(value)) # :nodoc: ## # Returns +true+ if the value is within EPSILON of one. def near_one?(value) = near_zero?(value - 1.0) # :nodoc: ## # Returns +true+ if the value is within EPSILON of one or more than one. def near_one_or_more?(value) = (value > 1.0 or near_one?(value)) # :nodoc: ## # Returns +true+ if the two values provided are near each other. def near?(x, y) = (x - y).abs <= Color::TOLERANCE # :nodoc: ## def to_degrees(radians) # :nodoc: if radians < 0 (Math::PI + radians % -Math::PI) * (180 / Math::PI) + 180 else (radians % Math::PI) * (180 / Math::PI) end end ## def to_radians(degrees) # :nodoc: degrees = ((degrees % 360) + 360) % 360 if degrees >= 180 Math::PI * (degrees - 360) / 180.0 else Math::PI * degrees / 180.0 end end ## # Normalizes the value to the range (0.0) .. (1.0). module_function def normalize(value, range = 0.0..1.0) # :nodoc: value = value.clamp(range) if near?(value, range.begin) range.begin elsif near?(value, range.end) range.end else value end end ## # Translates a value from range `from` to range `to`. Both ranges must be closed. # As 0.0 .. 1.0 is a common internal range, it is the default for `from`. # # This is based on the formula: # # [a, b] ← from ← [from.begin, from.end] # [c, d] ← to ← [to.begin, to.end] # # y = (((x - a) * (d - c)) / (b - a)) + c # # The value is clamped to the values of `to`. module_function def translate_range(x, to:, from: 0.0..1.0) # :nodoc: a, b = [from.begin, from.end] c, d = [to.begin, to.end] y = (((x - a) * (d - c)) / (b - a)) + c y.clamp(to) end ## # Normalizes the value to the specified range. def normalize_to_range(value, range) # :nodoc: range = (range.end..range.begin) if range.end < range.begin if value <= range.begin range.begin elsif value >= range.end range.end else value end end ## # Normalize the value to the range (0) .. (255). def normalize_byte(value) = normalize_to_range(value, 0..255).to_i # :nodoc: ## # Normalize the value to the range (0) .. (65535). def normalize_word(value) = normalize_to_range(value, 0..65535).to_i # :nodoc: end require "color/cmyk" require "color/grayscale" require "color/hsl" require "color/cielab" require "color/rgb" require "color/xyz" require "color/yiq" require "color/version" color-2.2.0/lib/color/0000755000004100000410000000000015137215736014557 5ustar www-datawww-datacolor-2.2.0/lib/color/grayscale.rb0000644000004100000410000001001015137215736017046 0ustar www-datawww-data# frozen_string_literal: true ## # \Grayscale is a color object representing shades of gray as a ratio of black to white, # where 0% (0.0) gray is black and 100% (1.0) gray is white. # # \Grayscale colors are immutable Data class instances. Array deconstruction is `[gray]` # and hash deconstruction is `{g:, gray:}`. See #g, #gray. class Color::Grayscale include Color ## # :attr_reader: brightness # Returns the grayscale value as a proportion of white (0.0 .. 1.0). ## # :attr_reader: g # Returns the grayscale value as a proportion of white (0.0 .. 1.0). ## # :attr_reader: gray # Returns the grayscale value as a percentage of white (0.0 .. 100.0). ## # Creates a grayscale color object from a percentage value (0.0 .. 100.0). # # ```ruby # Color::Grayscale.from_percentage(50) # => Grayscale [0.50%] # Color::Grayscale.from_values(50) # => Grayscale [0.50%] # ``` # # :call-seq: # from_percentage(g) # from_percentage(g:) # from_values(g) # from_values(g:) def self.from_percentage(*args, **kwargs) g = case [args, kwargs] in [[g], {}] g in [[], {g:}] g else new(*args, **kwargs) end new(g: g / 100.0) end class << self alias_method :from_values, :from_percentage alias_method :from_fraction, :new alias_method :from_internal, :from_fraction # :nodoc: end ## # Creates a grayscale color object from a fractional value (0.0 .. 1.0). # # ```ruby # Color::Grayscale.from_fraction(0.5) # Color::Grayscale.new(0.5) # Color::Grayscale[g: 0.5] # ``` def initialize(g:) super(g: normalize(g)) end ## # Coerces the other Color object to grayscale. def coerce(other) = other.to_grayscale ## # Convert \Grayscale to Color::CMYK. def to_cmyk(...) = Color::CMYK.from_fraction(0, 0, 0, 1.0 - g.to_f) ## # Convert \Grayscale to Color::RGB. def to_rgb(...) = Color::RGB.from_fraction(g, g, g) ## def to_grayscale(...) = self ## # Converts \Grayscale to Color::XYZ via Color::RGB. def to_xyz(...) = to_rgb(...).to_xyz(...) ## # Convert \Grayscale to Color::YIQ. # # This approximates the actual value, as I and Q are calculated by treating the # grayscale value as a RGB value. The Y (intensity or brightness) value is the same as # the grayscale value. def to_yiq(...) y = g i = (g * 0.596) + (g * -0.275) + (g * -0.321) q = (g * 0.212) + (g * -0.523) + (g * 0.311) Color::YIQ.from_fraction(y, i, q) end ## # Converts \Grayscale to Color::HSL. def to_hsl(...) = Color::HSL.from_fraction(0, 0, g) ## # Converts \Grayscale to Color::CIELAB via Color::RGB. def to_lab(...) = to_rgb(...).to_lab(...) ## # Present the color as an HTML/CSS color string (e.g., `#dddddd`). def html "##{("%02x" % translate_range(g, to: 0.0..255.0)) * 3}" end ## # Present the color as a CSS `rgb` color with optional `alpha`. # # ```ruby # Color::Grayscale[0.5].css # => rgb(50.00% 50.00% 50.00%) # Color::Grayscale[0.5].css(alpha: 0.75) # => rgb(50.00% 50.00% 50.00% / 0.75) # ``` def css(alpha: nil) params = ([css_value(gray, :percent)] * 3).join(" ") params = "#{params} / #{css_value(alpha)}" if alpha "rgb(#{params})" end ## # Lightens the grayscale color by the stated percent. def lighten_by(percent) = Color::Grayscale.from_fraction([g + (g * (percent / 100.0)), 1.0].min) ## # Darken the grayscale color by the stated percent. def darken_by(percent) = Color::Grayscale.from_fraction([g - (g * (percent / 100.0)), 0.0].max) ## alias_method :brightness, :g ## def gray = g * 100.0 ## def inspect = "Grayscale [%.2f%%]" % [gray] # :nodoc: ## def pretty_print(q) # :nodoc: q.text "Grayscale" q.breakable q.group 2, "[", "]" do q.text "%.2f%%" % gray end end ## def to_a = [gray] # :nodoc: ## alias_method :deconstruct, :to_a ## def deconstruct_keys(_keys) = {g:, gray:} ## def to_internal = [g] # :nodoc: ## def components = 1 # :nodoc: end color-2.2.0/lib/color/yiq.rb0000644000004100000410000000575015137215736015715 0ustar www-datawww-data# frozen_string_literal: true # A \Color object representing YIQ (NTSC) color encoding, where Y is the luma # (brightness) value, and I (orange-blue) and Q (purple-green) are chrominance. # # All values are clamped between 0 and 1 inclusive. # # More more details, see [YIQ][wikiyiq]. # # [wikiyiq]: https://en.wikipedia.org/wiki/YIQ # # \YIQ colors are immutable Data class instances. Array deconstruction is `[y, i, q]` and # hash deconstruction is `{y:, i:, q:}` (see #y, #i, #q). # # \YIQ is only partially implemented: other \Color objects can only be converted _to_ # \YIQ, but it has few conversion functions for converting _from_ \YIQ. class Color::YIQ include Color ## # :attr_reader: y # The `y` (luma) attribute of this \YIQ color expressed as a value 0..1. ## # :attr_reader: i # The `i` (orange-blue chrominance) attribute of this \YIQ color expressed as a value # 0..1. ## # :attr_reader: q # The `q` (purple-green chrominance) attribute of this \YIQ color expressed as a value # 0..1. ## # Creates a YIQ color object from percentage values 0 .. 1. # # ```ruby # Color::YIQ.from_percentage(30, 20, 10) # => YIQ [30% 20% 10%] # Color::YIQ.from_percentage(y: 30, i: 20, q: 10) # => YIQ [30% 20% 10%] # Color::YIQ.from_values(30, 20, 10) # => YIQ [30% 20% 10%] # Color::YIQ.from_values(y: 30, i: 20, q: 10) # => YIQ [30% 20% 10%] # ``` def self.from_percentage(*args, **kwargs) y, i, q = case [args, kwargs] in [[_, _, _], {}] args in [[], {y:, i:, q:}] [y, i, q] else new(*args, **kwargs) end new(y: y / 100.0, i: i / 100.0, q: q / 100.0) end class << self alias_method :from_values, :from_percentage alias_method :from_fraction, :new # :nodoc: alias_method :from_internal, :new end ## # Creates a YIQ color object from fractional values 0 .. 1. # # ```ruby # Color::YIQ.from_fraction(0.3, 0.2, 0.1) # => YIQ [30% 20% 10%] # Color::YIQ.new(0.3, 0.2, 0.1) # => YIQ [30% 20% 10%] # Color::YIQ[y: 0.3, i: 0.2, q: 0.1] # => YIQ [30% 20% 10%] # ``` def initialize(y:, i:, q:) # :nodoc: super(y: normalize(y), i: normalize(i), q: normalize(q)) end ## # Coerces the other Color object into \YIQ. def coerce(other) = other.to_yiq ## def to_yiq = self ## # Convert \YIQ to Color::Grayscale using the luma (#y) value. def to_grayscale = Color::Grayscale.from_fraction(y) ## alias_method :brightness, :y def inspect = "YIQ [%.2f%% %.2f%% %.2f%%]" % [y * 100, i * 100, q * 100] # :nodoc: def pretty_print(pq) # :nodoc: pq.text "YIQ" pq.breakable pq.group 2, "[", "]" do pq.text "%.2f%%" % (y * 100) pq.fill_breakable pq.text "%.2f%%" % (i * 100) pq.fill_breakable pq.text "%.2f%%" % (q * 100) end end alias_method :to_a, :deconstruct # :nodoc: alias_method :to_internal, :deconstruct # :nodoc: def deconstruct_keys(_keys) = {y:, i:, q:} # :nodoc: end color-2.2.0/lib/color/xyz.rb0000644000004100000410000002244315137215736015743 0ustar www-datawww-data# frozen_string_literal: true ## # A \Color object for the CIE 1931 \XYZ color space derived from the original CIE \RGB # color space as linear transformation functions x̅(λ), y̅(λ), and z̅(λ) that describe the # device-independent \CIE standard observer. It underpins most other CIE color systems # (such as \CIELAB), but is directly used mostly for color instrument readings and color # space transformations particularly in color profiles. # # The \XYZ color space ranges describe the mixture of wavelengths of light required to # stimulate cone cells in the human eye, as well as the luminance (brightness) required. # The `Y` component describes the luminance while the `X` and `Z` components describe two # axes of chromaticity. Definitionally, the minimum value for any \XYZ color component is # 0. # # As \XYZ describes imaginary colors, the color gamut is usually expressed in relation to # a reference white of an illuminant (frequently often D65 or D50) and expressed as the # `xyY` color space, computed as: # # ``` # x = X / (X + Y + Z) # y = Y / (X + Y + Z) # Y = Y # ``` # # The range of `Y` values is conventionally clamped to 0..100, whereas the `X` and `Z` # values must be no lower than 0 and on the same scale. # # For more details, see [CIE XYZ color space][ciexyz]. # # [ciexyz]: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_XYZ_color_space # # \XYZ colors are immutable Data class instances. Array deconstruction is `[x * 100, # y * 100, z * 100]` and hash deconstruction is `{x:, y:, z:}` (see #x, #y, #z). class Color::XYZ include Color ## # :attr_reader: x # The X attribute of this \XYZ color object expressed as a value scaled to #y. ## # :attr_reader: y # The Y attribute of this \XYZ color object expressed as a value 0..1. ## # :attr_reader: z # The Z attribute of this \XYZ color object expressed as a value scaled to #y. ## # Creates a \XYZ color representation from native values. `y` must be between 0 and 100 # and `x` and `z` values must be scaled to `y`. # # ```ruby # Color::XYZ.from_values(95.047, 100.00, 108.883) # Color::XYZ.from_values(x: 95.047, y: 100.00, z: 108.883) # ``` # # call-seq: # Color::XYZ.from_values(x, y, z) # Color::XYZ.from_values(x:, y:, z:) def self.from_values(*args, **kwargs) x, y, z = case [args, kwargs] in [[_, _, _], {}] args in [[], {x:, y:, z:}] [x, y, z] else new(*args, **kwargs) end new(x: x / 100.0, y: y / 100.0, z: z / 100.0) end class << self alias_method :from_fraction, :new alias_method :from_internal, :new # :nodoc: end ## # Creates a \XYZ color representation from native values. The `y` value must be between # 0 and 1 and `x` and `z` must be fractional valiues greater than or equal to 0. # # ```ruby # Color::XYZ.from_fraction(0.95047, 1.0, 1.0883) # Color::XYZ.new(0.95047, 1.0, 1.08883) # Color::XYZ[x: 0.95047, y: 1.0, z: 1.08883] # ``` def initialize(x:, y:, z:) # The X and Z values in the XYZ color model are technically unbounded. With Y scaled # to 1.0, we will clamp X to 0.0..2.2 and Z to 0.0..2.8. super( x: normalize(x, 0.0..2.2), y: normalize(y), z: normalize(z, 0.0..2.8) ) end # :stopdoc: # NOTE: This should be using Rational instead of floating point values, # otherwise there will be discontinuities. # http://www.brucelindbloom.com/LContinuity.html # :startdoc: E = 216r/24389r # :nodoc: K = 24389r/27r # :nodoc: EK = E * K # :nodoc: ## # White points for standard illuminants at 2° (CIE 1931). WP2 = { A: new(1.09849161234507, 1.0, 0.355798257454902), B: new(0.9909274480248, 1.0, 0.853132732288615), C: new(0.980705971659919, 1.0, 1.18224949392713), D50: new(0.964211994421199, 1.0, 0.825188284518828), D55: new(0.956797052643698, 1.0, 0.921480586017327), D65: new(0.950430051970945, 1.0, 1.08880649180926), D75: new(0.949722089884072, 1.0, 1.22639352072415), D93: new(0.953014035205816, 1.0, 1.41274275520851), E: new(1, 1.0, 1.0000300003), F1: new(0.92833634773327, 1.0, 1.03664719660806), F2: new(0.991446614618029, 1.0, 0.673159423379253), F3: new(1.03753487192493, 1.0, 0.49860512300279), F4: new(1.0914726375561, 1.0, 0.388132609288601), F5: new(0.908719701138108, 1.0, 0.987228866815325), F6: new(0.973091283635895, 1.0, 0.601905497618128), F7: new(0.950171560440895, 1.0, 1.08629642000425), F8: new(0.96412543554007, 1.0, 0.823331010452962), F9: new(1.00364797081623, 1.0, 0.678683511708377), F10: new(0.961735119213027, 1.0, 0.817123325737787), F11: new(1.00898894280487, 1.0, 0.642616604353936), F12: new(1.08046289656537, 1.0, 0.392275166291635), "FL3.0": new(1.09273493677163, 1.0, 0.3868088271758), "FL3.1": new(1.01981788966256, 1.0, 0.658275307980718), "FL3.2": new(0.916836289619075, 1.0, 0.990985751671998), "FL3.3": new(1.09547365817462, 1.0, 0.377937175364828), "FL3.4": new(1.02096949891068, 1.0, 0.702342047930283), "FL3.5": new(0.968888888888889, 1.0, 0.808888888888889), "FL3.6": new(1.08380716934487, 1.0, 0.388380716934487), "FL3.7": new(0.996868475991649, 1.0, 0.612734864300626), "FL3.8": new(0.974380395433027, 1.0, 0.810359231411863), "FL3.9": new(0.970505617977528, 1.0, 0.838483146067416), "FL3.10": new(0.944962143273151, 1.0, 0.967093768200349), "FL3.11": new(1.08422095615556, 1.0, 0.392865989596235), "FL3.12": new(1.02846401718582, 1.0, 0.656820622986037), "FL3.13": new(0.955112219451372, 1.0, 0.815738431698531), "FL3.14": new(0.951034063260341, 1.0, 1.09032846715328), HP1: new(1.28433734939759, 1.0, 0.125301204819277), HP2: new(1.14911014911015, 1.0, 0.255892255892256), HP3: new(1.05570552147239, 1.0, 0.398282208588957), HP4: new(1.00395048722676, 1.0, 0.62970766394522), HP5: new(1.01696741179639, 1.0, 0.676272555884729), "LED-B1": new(1.11819519372241, 1.0, 0.3339872486513), "LED-B2": new(1.08599202392822, 1.0, 0.406530408773679), "LED-B3": new(1.0088638195004, 1.0, 0.677142089712597), "LED-B4": new(0.977155910908053, 1.0, 0.87835522558538), "LED-B5": new(0.963535228677379, 1.0, 1.12669962917182), "LED-BH1": new(1.10034431874078, 1.0, 0.359075258239056), "LED-RGB1": new(1.08216575635241, 1.0, 0.292567086202802), "LED-V1": new(1.12462908011869, 1.0, 0.348170128585559), "LED-V2": new(1.00158940397351, 1.0, 0.647417218543046), ID50: new(0.952803997779012, 1.0, 0.823431426985008), ID65: new(0.939522225582099, 1.0, 1.08436649531297) }.freeze ## # The D50 standard illuminant white point at 2° (CIE 1931). D50 = WP2[:D50] ## # The D65 standard illuminant white point at 2° (CIE 1931). D65 = WP2[:D65] ## # Coerces the other Color object into \XYZ. def coerce(other) = other.to_xyz ## def to_xyz(...) = self ## # Converts \XYZ to Color::CMYK via Color::RGB. # # See #to_rgb and Color::RGB#to_cmyk. def to_cmyk(...) = to_rgb(...).to_cmyk(...) ## # Converts \XYZ to Color::Grayscale using the #y value def to_grayscale(...) = Color::Grayscale.from_fraction(y) ## # Converts \XYZ to Color::HSL via Color::RGB. # # See #to_rgb and Color::RGB#to_hsl. def to_hsl(...) = to_rgb(...).to_hsl(...) ## # Converts \XYZ to Color::YIQ via Color::RGB. # # See #to_rgb and Color::RGB#to_yiq. def to_yiq(...) = to_rgb(...).to_yiq(...) ## # Converts \XYZ to Color::CIELAB. # # :call-seq: # to_lab(white: Color::XYZ::D65) def to_lab(*args, **kwargs) ref = kwargs[:white] || args.first || Color::XYZ::D65 # Calculate the ratio of the XYZ values to the reference white. # http://www.brucelindbloom.com/index.html?Equations.html rel = scale(1.0 / ref.x, 1.0 / ref.y, 1.0 / ref.z) # And now transform # https://en.wikipedia.org/wiki/Lab_color_space#Forward_transformation # There is a brief explanation there as far as the nature of the calculations, # as well as a much nicer looking modeling of the algebra. f = rel.map { |t| if t > E t**(1.0 / 3) else # t <= E ((K * t) + 16) / 116.0 # The 4/29 here is for when t = 0 (black). 4/29 * 116 = 16, and 16 - # 16 = 0, which is the correct value for L* with black. # ((1.0/3)*((29.0/6)**2) * t) + (4.0/29) end } Color::CIELAB.from_values( (116 * f.y) - 16, 500 * (f.x - f.y), 200 * (f.y - f.z) ) end ## # Converts \XYZ to Color::RGB. # # This always assumes an sRGB target color space and a D65 white point. def to_rgb(...) # sRGB companding from linear values linear = [ x * 3.2406255 + y * -1.5372080 + z * -0.4986286, x * -0.9689307 + y * 1.8757561 + z * 0.0415175, x * 0.0557101 + y * -0.2040211 + z * 1.0569959 ].map { if _1 <= 0.0031308 _1 * 12.92 else 1.055 * (_1**(1 / 2.4)) - 0.055 end } Color::RGB.from_fraction(*linear) end def deconstruct = [x * 100.0, y * 100.0, z * 100.0] # :nodoc: alias_method :to_a, :deconstruct # :nodoc: def to_internal = [x, y, z] # :nodoc: def inspect = "XYZ [%.4f %.4f %.4f]" % [x, y, z] # :nodoc: def pretty_print(q) # :nodoc: q.text "XYZ" q.breakable q.group 2, "[", "]" do q.text "%.4f" % x q.fill_breakable q.text "%.4f" % y q.fill_breakable q.text "%.4f" % z end end end color-2.2.0/lib/color/cielab.rb0000644000004100000410000002643515137215736016335 0ustar www-datawww-data# frozen_string_literal: true ## # A \Color object for the \CIELAB color space (also known as L\*a\*\b*). Color is # expressed in a three-dimensional, device-independent "standard observer" model, often # in relation to a "reference white" color, usually Color::XYZ::D65 (most purposes) or # Color::XYZ::D50 (printing). # # `L*` is the perceptual lightness, bounded to values between 0 (black) and 100 (white). # `a*` is the range of green (negative) / red (positive) and `b*` is the range of blue # (negative) / yellow (positive). # # The `a*` and `b*` ranges are _technically_ unbounded but \Color clamps them to the # values `-128..127`. # # For more information, see [CIELAB](https://en.wikipedia.org/wiki/CIELAB_color_space). # # \CIELAB colors are immutable Data class instances. Array deconstruction is `[l, a, b]` # and hash deconstruction is `{l:, a:, b:}` (see #l, #a, #b). class Color::CIELAB include Color ## # Standard weights applied for perceptual differences using the ΔE*94 algorithm. DE94_WEIGHTS = { graphic_arts: {k_1: 0.045, k_2: 0.015, k_l: 1.0}.freeze, textiles: {k_1: 0.048, k_2: 0.014, k_l: 2.0}.freeze }.freeze RANGES = {L: 0.0..100.0, ab: -128.0..127.0}.freeze # :nodoc: private_constant :RANGES ## # :attr_reader: l # The `L*` attribute of this \CIELAB color object expressed as a value 0..100. ## # :attr_reader: a # The `a*` attribute of this \CIELAB color object expressed as a value -128..127. ## # :attr_reader: b # The `b*` attribute of this \CIELAB color object expressed as a value -128..127. ## # Creates a \CIELAB color representation from percentage values. # # `l` must be between 0% and 100%; `a` and `b` must be between -100% and 100% and will # be transposed to the native value -128..127. # # ```ruby # Color::CIELAB.from_percentage(10, -30, 30) # => CIELAB [10.0000 -38.7500 37.7500] # ``` # # :call-seq: # from_percentage(l, a, b) # from_percentage(l:, a:, b:) def self.from_percentage(*args, **kwargs) l, a, b = case [args, kwargs] in [[_, _, _], {}] args in [[], {l:, a:, b:}] [l, a, b] else new(*args, **kwargs) end new( l: l, a: Color.translate_range(a, from: -100.0..100.0, to: RANGES[:ab]), b: Color.translate_range(b, from: -100.0..100.0, to: RANGES[:ab]) ) end class << self alias_method :from_values, :new alias_method :from_internal, :new # :nodoc: end ## # Creates a \CIELAB color representation from `L*a*b*` native values. The `l` value # must be between 0 and 100 and the `a` and `b` values must be between -128 and 127. # # ```ruby # Color::CIELAB.new(10, 35, -35) # => CIELAB [10.00 35.00 -35.00] # Color::CIELAB.from_values(10, 35, -35) # => CIELAB [10.00 35.00 -35.00] # Color::CIELAB[l: 10, a: 35, b: -35] # => CIELAB [10.00 35.00 -35.00] # ``` def initialize(l:, a:, b:) super( l: normalize(l, RANGES[:L]), a: normalize(a, RANGES[:ab]), b: normalize(b, RANGES[:ab]) ) end ## # Coerces the other Color object into \CIELAB. def coerce(other) = other.to_lab ## # Converts \CIELAB to Color::CMYK via Color::RGB. # # See #to_rgb and Color::RGB#to_cmyk. def to_cmyk(...) = to_rgb(...).to_cmyk(...) ## # Converts \CIELAB to Color::Grayscale via Color::RGB. # # See #to_rgb and Color::RGB#to_grayscale. def to_grayscale(...) = to_rgb(...).to_grayscale(...) ## def to_lab(...) = self ## # Converts \CIELAB to Color::HSL via Color::RGB. # # See #to_rgb and Color::RGB#to_hsl. def to_hsl(...) = to_rgb(...).to_hsl(...) ## # Converts \CIELAB to Color::RGB via Color::XYZ. # # See #to_xyz and Color::XYZ#to_rgb. def to_rgb(...) = to_xyz(...).to_rgb(...) ## # Converts \CIELAB to Color::XYZ based on a reference white. # # Accepts a single keyword parameter, `white`, indicating the reference white used for # conversion scaling. If none is provided, Color::XYZ::D65 is used. # # :call-seq: # to_xyz(white: Color::XYZ::D65) def to_xyz(*args, **kwargs) fy = (l + 16.0) / 116 fz = fy - b / 200.0 fx = a / 500.0 + fy xr = ((fx3 = fx**3) > Color::XYZ::E) ? fx3 : (116.0 * fx - 16) / Color::XYZ::K yr = (l > Color::XYZ::EK) ? ((l + 16.0) / 116)**3 : l / Color::XYZ::K zr = ((fz3 = fz**3) > Color::XYZ::E) ? fz3 : (116.0 * fz - 16) / Color::XYZ::K ref = kwargs[:white] || args.first ref = Color::XYZ::D65 unless ref.is_a?(Color::XYZ) ref.scale(xr, yr, zr) end ## # Converts \CIELAB to Color::YIQ via Color::XYZ. def to_yiq(...) = to_xyz(...).to_yiq(...) ## # Render the CSS `lab()` function for this \CIELAB object, adding an `alpha` if # provided. def css(alpha: nil, **) params = [css_value(l, :percent), css_value(a), css_value(b)].join(" ") params = "#{params} / #{css_value(alpha)}" if alpha "lab(#{params})" end ## # Implements the \CIELAB ΔE* 2000 perceptual color distance metric with more reliable # results over \CIELAB ΔE* 1994. # # See [CIEDE2000][ciede2000] for precise details on the mathematical formulas. The # implementation here is based on Sharma, Wu, and Dala in [CIEDE2000.xls][ciede2000xls], # published as supplementary materials for their paper "The CIEDE2000 Color-Difference # Formula: Implementation Notes, Supplementary Test Data, and Mathematical # Observations,", G. Sharma, W. Wu, E. N. Dalal, Color Research and Application, vol. # 30. No. 1, pp. 21-30, February 2005. # # Do not override the `klch` parameter unless you _really_ know what you're doing. # # See also # # [ciede2000]: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 # [ciede2000xls]: http://www.ece.rochester.edu/~gsharma/ciede2000/dataNprograms/CIEDE2000.xls def delta_e2000(other, klch: {L: 1.0, C: 1.0, H: 1.0}) other = coerce(other) klch => L: k_l, C: k_c, H: k_h self => l: l_star_1, a: a_star_1, b: b_star_1 other => l: l_star_2, a: a_star_2, b: b_star_2 v_25_pow_7 = 25**7 c_star_1 = Math.sqrt(a_star_1**2 + b_star_1**2) c_star_2 = Math.sqrt(a_star_2**2 + b_star_2**2) c_mean = ((c_star_1 + c_star_2) / 2.0) c_mean_pow_7 = c_mean**7 c_mean_g = (0.5 * (1.0 - Math.sqrt(c_mean_pow_7 / (c_mean_pow_7 + v_25_pow_7)))) a_1_prime = ((1.0 + c_mean_g) * a_star_1) a_2_prime = ((1.0 + c_mean_g) * a_star_2) c_1_prime = Math.sqrt(a_1_prime**2 + b_star_1**2) c_2_prime = Math.sqrt(a_2_prime**2 + b_star_2**2) h_1_prime = if a_1_prime + b_star_1 == 0 0 else (to_degrees(Math.atan2(b_star_1, a_1_prime)) % 360.0) end h_2_prime = if a_2_prime + b_star_2 == 0 0 else (to_degrees(Math.atan2(b_star_2, a_2_prime)) % 360.0) end delta_lower_h_prime = if h_2_prime - h_1_prime < -180 h_2_prime + 360 - h_1_prime elsif h_2_prime - h_1_prime > 180 h_2_prime - h_1_prime - 360.0 else h_2_prime - h_1_prime end delta_upper_l_prime = l_star_2 - l_star_1 delta_upper_c_prime = c_2_prime - c_1_prime delta_upper_h_prime = ( 2.0 * Math.sqrt(c_1_prime * c_2_prime) * Math.sin(to_radians(delta_lower_h_prime / 2.0)) ) l_prime_mean = ((l_star_1 + l_star_2) / 2.0) c_prime_mean = ((c_1_prime + c_2_prime) / 2.0) h_prime_mean = if c_1_prime * c_2_prime == 0 h_1_prime + h_2_prime elsif (h_2_prime - h_1_prime).abs <= 180 ((h_1_prime + h_2_prime) / 2.0) elsif h_2_prime + h_1_prime <= 360 ((h_1_prime + h_2_prime) / 2.0 + 180.0) else ((h_1_prime + h_2_prime) / 2.0 - 180.0) end l_prime_mean50sq = ((l_prime_mean - 50)**2) upper_s_l = (1 + (0.015 * l_prime_mean50sq / Math.sqrt(20 + l_prime_mean50sq))) upper_s_c = (1 + 0.045 * c_prime_mean) upper_t = ( 1 - 0.17 * Math.cos(to_radians(h_prime_mean - 30)) + 0.24 * Math.cos(to_radians(2 * h_prime_mean)) + 0.32 * Math.cos(to_radians(3 * h_prime_mean + 6)) - 0.2 * Math.cos(to_radians(4 * h_prime_mean - 63)) ) upper_s_h = (1 + 0.015 * c_prime_mean * upper_t) delta_theta = (30 * Math.exp(-1 * ((h_prime_mean - 275) / 25.0)**2)) upper_r_c = (2 * Math.sqrt(c_prime_mean**7 / (c_prime_mean**7 + v_25_pow_7))) upper_r_t = (-Math.sin(to_radians(2 * delta_theta)) * upper_r_c) delta_l_prime_div_kl_div_sl = (delta_upper_l_prime / upper_s_l / k_l.to_f) delta_c_prime_div_kc_div_sc = (delta_upper_c_prime / upper_s_c / k_c.to_f) delta_h_prime_div_kh_div_sh = (delta_upper_h_prime / upper_s_h / k_h.to_f) Math.sqrt( delta_l_prime_div_kl_div_sl**2 + delta_c_prime_div_kc_div_sc**2 + delta_h_prime_div_kh_div_sh**2 + upper_r_t * delta_c_prime_div_kc_div_sc * delta_h_prime_div_kh_div_sh ) end ## # Implements the \CIELAB ΔE* 1994 perceptual color distance metric. This version is an # improvement over previous versions, but it does not handle perceptual discontinuities # as well as \CIELAB ΔE* 2000. This is implemented because some functions still require # the 1994 algorithm for proper operation. # # See [CIE94][cie94] for precise details on the mathematical formulas. # # Different weights for `k_l`, `k_1`, and `k_2` may be applied via the `weight` keyword # parameter. This may be provided either as a Hash with `k_l`, `k_1`, and `k_2` values # or as a key to DE94_WEIGHTS. The default weight is `:graphic_arts`. # # See also . # # [cie94]: https://en.wikipedia.org/wiki/Color_difference#CIE94 def delta_e94(other, weight: :graphic_arts) weight = DE94_WEIGHTS[weight] if DE94_WEIGHTS.key?(weight) raise ArgumentError, "Unsupported weight #{weight.inspect}." unless weight.is_a?(Hash) weight => k_1:, k_2:, k_l: # Under some circumstances in real computers, the computed value of ΔH could be an # imaginary number (it's a square root value), so instead of √(((ΔL/(kL*sL))²) + # ((ΔC/(kC*sC))²) + ((ΔH/(kH*sH))²)), we have implemented the final computation as # √(((ΔL/(kL*sL))²) + ((ΔC/(kC*sC))²) + (ΔH2/(kH*sH)²)) and not performing the square # root when computing ΔH2. k_c = k_h = 1.0 other = coerce(other) self => l: l_1, a: a_1, b: b_1 other => l: l_2, a: a_2, b: b_2 delta_a = a_1 - a_2 delta_b = b_1 - b_2 cab_1 = Math.sqrt((a_1**2) + (b_1**2)) cab_2 = Math.sqrt((a_2**2) + (b_2**2)) delta_upper_l = l_1 - l_2 delta_upper_c = cab_1 - cab_2 delta_h2 = (delta_a**2) + (delta_b**2) - (delta_upper_c**2) s_upper_l = 1.0 s_upper_c = 1 + k_1 * cab_1 s_upper_h = 1 + k_2 * cab_1 composite_upper_l = (delta_upper_l / (k_l * s_upper_l))**2 composite_upper_c = (delta_upper_c / (k_c * s_upper_c))**2 composite_upper_h = delta_h2 / ((k_h * s_upper_h)**2) Math.sqrt(composite_upper_l + composite_upper_c + composite_upper_h) end ## alias_method :to_a, :deconstruct ## alias_method :to_internal, :deconstruct # :nodoc: ## def inspect = "CIELAB [%.4f %.4f %.4f]" % [l, a, b] # :nodoc: ## def pretty_print(q) # :nodoc: q.text "CIELAB" q.breakable q.group 2, "[", "]" do q.text "%.4f" % l q.fill_breakable q.text "%.4f" % a q.fill_breakable q.text "%.4f" % b end end end color-2.2.0/lib/color/rgb.rb0000644000004100000410000004757015137215736015673 0ustar www-datawww-data# frozen_string_literal: true # The \RGB color model is an additive color model where the primary colors (red, green, # and blue) of light are added to produce millions of colors. \RGB rendering is # device-dependent and without color management, the same "red" color will render # differently. # # This class does not implement color management and is not \RGB colorspace aware; that is, # unless otherwise noted, it does not assume that the \RGB represented is sRGB or Adobe # \RGB (opRGB). # # \RGB colors are immutable Data class instances. Array deconstruction is `[red, green, # blue]` and hash deconstruction is `{r:, red:, g:, green:, b:, blue}`. See #r, #red, #g, # #green, #b, #blue. class Color::RGB include Color ## # :attr_reader: r # Returns the red component of the color in the range 0.0..1.0. ## # :attr_reader: red # Returns the red component of the color in the normal 0..255 range. ## # :attr_reader: red_p # Returns the red component of the color as a percentage (0.0 .. 100.0). # ## # :attr_reader: g # Returns the green component of the color in the range 0.0..1.0. ## # :attr_reader: green # Returns the green component of the color in the normal 0 .. 255 range. ## # :attr_reader: green_p # Returns the green component of the color as a percentage (0.0 .. 100.0). ## # :attr_reader: b # Returns the blue component of the color in the range 0.0..1.0. ## # :attr_reader: blue # Returns the blue component of the color in the normal 0 .. 255 range. ## # :attr_reader: blue_p # Returns the blue component of the color as a percentage (0.0 .. 100.0). ## # Creates a \RGB color object from fractional values (0.0 .. 1.0). # # ```ruby # Color::RGB.from_fraction(0.3, 0.2, 0.1) # => RGB [#4d331a] # Color::RGB.new(0.3, 0.2, 0.1) # => RGB [#4d331a] # Color::RGB[r: 0.3, g: 0.2, b: 0.1] # => RGB [#4d331a] # ``` def initialize(r:, g:, b:, names: nil) super(r: normalize(r), g: normalize(g), b: normalize(b), names: names) end Black000 = new(r: 0x00, g: 0x00, b: 0x00) # :nodoc: WhiteFFF = new(r: 0xff, g: 0xff, b: 0xff) # :nodoc: ## # :attr_reader: name # The primary name for this \RGB color. # # If there are no defined names, the color will be checked in the name registry and if # there's a match, it will be returned. ## # :attr_reader: names # The defined names for this \RGB color. ## def name # :nodoc: name = names&.first return name if name self.class.send(:__by_hex)[hex]&.name if defined?(Color::RGB::Metallic) end ## # Coerces the other Color object into \RGB. def coerce(other) = other.to_rgb ## # Converts the \RGB color to Color::CMYK. # # Most color experts strongly suggest that this is not a good idea (some suggesting that # it's a very bad idea). CMYK represents additive percentages of inks on white paper, # whereas \RGB represents mixed color intensities on an unlit (black) screen. # # 1. Convert the R, G, and B components to C, M, and Y components. # # c = 1.0 - r # m = 1.0 - g # y = 1.0 - b # # 2. Compute the minimum amount of black (K) required to smooth the color in inks. # # k = min(c, m, y) # # 3. Perform undercolor removal on the C, M, and Y components of the colors because less # of each color is needed for each bit of black. Also, regenerate the black (K) based # on the undercolor removal so that the color is more accurately represented in ink. # # c = min(1.0, max(0.0, c - UCR(k))) # m = min(1.0, max(0.0, m - UCR(k))) # y = min(1.0, max(0.0, y - UCR(k))) # k = min(1.0, max(0.0, BG(k))) # # The undercolor removal function and the black generation functions return a value # based on the brightness of the \RGB color. def to_cmyk(...) c = 1.0 - r.to_f m = 1.0 - g.to_f y = 1.0 - b.to_f k = [c, m, y].min k -= (k * brightness) c = normalize(c - k) m = normalize(m - k) y = normalize(y - k) k = normalize(k) Color::CMYK.from_fraction(c, m, y, k) end ## def to_rgb(...) = self ## # Convert \RGB to Color::Grayscale via Color::HSL (for the luminance value). def to_grayscale(...) = Color::Grayscale.from_fraction(to_hsl.l) ## # Converts \RGB to Color::YIQ. def to_yiq(...) y = (r * 0.299) + (g * 0.587) + (b * 0.114) i = (r * 0.596) + (g * -0.275) + (b * -0.321) q = (r * 0.212) + (g * -0.523) + (b * 0.311) Color::YIQ.from_fraction(y, i, q) end ## # Converts \RGB to Color::HSL. # # The conversion here is based on formulas from https://www.easyrgb.com/math.php and # elsewhere. def to_hsl(...) min, max = [r, g, b].minmax delta = (max - min).to_f l = (max + min) / 2.0 if near_zero?(delta) # close to 0.0, so it's a gray h = 0 s = 0 else s = if near_zero_or_less?(l - 0.5) delta / (max + min).to_f else delta / (2 - max - min).to_f end # This is based on the conversion algorithm from # https://en.wikipedia.org/wiki/HSV_color_space#Conversion_from_RGB_to_HSL_or_HSV # Contributed by Adam Johnson sixth = 1 / 6.0 if r == max # near_zero_or_less?(r - max) h = (sixth * ((g - b) / delta)) h += 1.0 if g < b elsif g == max # near_zero_or_less(g - max) h = (sixth * ((b - r) / delta)) + (1.0 / 3.0) elsif b == max # near_zero_or_less?(b - max) h = (sixth * ((r - g) / delta)) + (2.0 / 3.0) end h += 1 if h < 0 h -= 1 if h > 1 end Color::HSL.from_fraction(h, s, l) end ## # Converts \RGB to Color::XYZ using the D65 reference white. This is based on conversion # formulas presented by Bruce Lindbloom, in particular [RGB to XYZ][rgbxyz]. # # [rgbxyz]: http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html # # The conversion is performed assuming the \RGB value is in the sRGB color space. No # other \RGB color spaces are currently supported. # # :call-seq: # to_xyz(color_space: :sRGB) def to_xyz(*args, **kwargs) color_space = kwargs[:color_space] || args.first || :sRGB case color_space.to_s.downcase when "srgb" # Inverse sRGB companding. Linearizes RGB channels with respect to energy. rr, gg, bb = [r, g, b].map { if _1 > 0.04045 (((_1 + 0.055) / 1.055)**2.4) else (_1 / 12.92) end * 100.0 } # Convert using the RGB/XYZ matrix at: # http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices Color::XYZ.from_values( rr * 0.4124564 + gg * 0.3575761 + bb * 0.1804375, rr * 0.2126729 + gg * 0.7151522 + bb * 0.0721750, rr * 0.0193339 + gg * 0.1191920 + bb * 0.9503041 ) else raise ArgumentError, "Unsupported color space #{color_space}." end end ## # Converts \RGB to Color::CIELAB via Color::XYZ. # # Based on the [XYZ to CIELAB][xyztolab] formula presented by Bruce Lindbloom. # # [xyztolab]: http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html # # The conversion is performed assuming the \RGB value is in the sRGB color space. No # other \RGB color spaces are currently supported. By default, uses the D65 reference # white for the conversion. # # :call-seq: # to_lab(color_space: :sRGB, white: Color::XYZ::D65) def to_lab(...) = to_xyz(...).to_lab(...) ## # Present the color as an HTML/CSS \RGB hex triplet (+ccddee+). def hex "%02x%02x%02x" % [red, green, blue].map(&:round) end ## # Present the color as an HTML/CSS color string (+#ccddee+). def html "##{hex}" end ## # Present the color as an CSS `rgb` function with optional `alpha`. # # ```ruby # rgb = Color::RGB.from_percentage(0, 50, 100) # rgb.css # => rgb(0 50.00% 100.00%) # rgb.css(alpha: 0.5) # => rgb(0 50.00% 100.00% / 0.50) # ``` def css(alpha: nil) params = [css_value(red_p, :percent), css_value(green_p, :percent), css_value(blue_p, :percent)].join(" ") params = "#{params} / #{css_value(alpha)}" if alpha "rgb(#{params})" end ## # Computes the ΔE* 2000 difference via Color::CIELAB. See Color::CIELAB#delta_e2000. def delta_e2000(other) = to_lab.delta_e2000(coerce(other).to_lab) ## # Mix the \RGB hue with white so that the \RGB hue is the specified percentage of the # resulting color. # # Strictly speaking, this isn't a `lighten_by` operation, but it mostly works. def lighten_by(percent) = mix_with(Color::RGB::WhiteFFF, percent) ## # Mix the \RGB hue with black so that the \RGB hue is the specified percentage of the # resulting color. # # Strictly speaking, this isn't a `darken_by` operation, but it mostly works. def darken_by(percent) = mix_with(Color::RGB::Black000, percent) ## # Mix the mask color with the current color at the stated opacity percentage (0..100). def mix_with(mask, opacity) opacity = normalize(opacity / 100.0) mask = coerce(mask) with( r: (r * opacity) + (mask.r * (1 - opacity)), g: (g * opacity) + (mask.g * (1 - opacity)), b: (b * opacity) + (mask.b * (1 - opacity)) ) end ## # Returns the brightness value for a color, a number between 0..1. # # Based on the Y value of Color::YIQ encoding, representing luminosity, or perceived # brightness. def brightness = to_yiq.y ## # Returns a new \RGB color with the brightness adjusted by the specified percentage via # Color::HSL. Negative percentages will darken the color; positive percentages will # brighten the color. # # ```ruby # dark_blue = Color::RGB::DarkBlue # => RGB [#00008b] # dark_blue.adjust_brightness(10) # => RGB [#000099] # dark_blue.adjust_brightness(-10) # => RGB [#00007d] # ``` def adjust_brightness(percent) hsl = to_hsl hsl.with(l: hsl.l * percent_adjustment(percent)).to_rgb end ## # Returns a new \RGB color with the saturation adjusted by the specified percentage via # Color::HSL. Negative percentages will reduce the saturation; positive percentages will # increase the saturation. # # ```ruby # dark_blue = Color::RGB::DarkBlue # => RGB [#00008b] # dark_blue.adjust_saturation(10) # => RGB [#00008b] # dark_blue.adjust_saturation(-10) # => RGB [#070784] # ``` def adjust_saturation(percent) hsl = to_hsl hsl.with(s: hsl.s * percent_adjustment(percent)).to_rgb end ## # Returns a new \RGB color with the hue adjusted by the specified percentage via # Color::HSL. Negative percentages will reduce the hue; positive percentages will # increase the hue. # # ```ruby # dark_blue = Color::RGB::DarkBlue # => RGB [#00008b] # dark_blue.adjust_hue(10) # => RGB [#38008b] # dark_blue.adjust_hue(-10) # => RGB [#00388b] # ``` def adjust_hue(percent) hsl = to_hsl hsl.with(h: hsl.h * percent_adjustment(percent)).to_rgb end ## # Determines the closest match to this color from a list of provided colors or `nil` if # `color_list` is empty or no color is found within the `threshold_distance`. # # The default search uses the CIE ΔE* 1994 algorithm (CIE94) to find near matches based # on the perceived visual differences between the colors. The default value for # `algorithm` is `:delta_e94`. # # `threshold_distance` is used to determine the minimum color distance permitted. Uses # the CIE ΔE* 1994 algorithm (CIE94) to find near matches based on perceived visual # color. The default value (1000.0) is an arbitrarily large number. The values `:jnd` # and `:just_noticeable` may be passed as the `threshold_distance` to use the value # `2.3`. # # All ΔE* formulae were designed to use 1.0 as a "just noticeable difference" (JND), # but CIE ΔE*ab 1976 defined JND as 2.3. # # :call-seq: # closest_match(color_list, algorithm: :delta_e94, threshold_distance: 1000.0) def closest_match(color_list, *args, **kwargs) color_list = [color_list].flatten(1) return nil if color_list.empty? algorithm = kwargs[:algorithm] || args.first || :delta_e94 threshold_distance = kwargs[:threshold_distance] || args[1] || 1000.0 threshold_distance = case threshold_distance when :jnd, :just_noticeable 2.3 else threshold_distance.to_f end closest_distance = threshold_distance best_match = nil color_list.each do |c| distance = contrast(c, algorithm) if distance < closest_distance closest_distance = distance best_match = c end end best_match end ## # The Delta E (CIE94) algorithm http://en.wikipedia.org/wiki/Color_difference#CIE94 # # There is a newer version, CIEDE2000, that uses slightly more complicated math, but # addresses "the perceptual uniformity issue" left lingering by the CIE94 algorithm. # # Since our source is treated as sRGB, we use the "graphic arts" presets for k_L, k_1, # and k_2 # # The calculations go through LCH(ab). (?) # # See also http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html def delta_e94(...) = to_lab.delta_e94(...) ## def red = normalize(r * 255.0, 0.0..255.0) # :nodoc: ## def red_p = normalize(r * 100.0, 0.0..100.0) # :nodoc: ## def green = normalize(g * 255.0, 0.0..255.0) # :nodoc: ## def green_p = normalize(g * 100.0, 0.0..100.0) # :nodoc: ## def blue = normalize(b * 255.0, 0.0..255.0) # :nodoc: ## def blue_p = normalize(b * 100.0, 0.0..100.0) # :nodoc: ## # Return a Grayscale color object created from the largest of the `r`, `g`, and `b` # values. def max_rgb_as_grayscale = Color::Grayscale.from_fraction([r, g, b].max) ## def inspect = names ? "RGB [#{html}] {#{names.join(" ")}}" : "RGB [#{html}]" # :nodoc: ## def pretty_print(q) # :nodoc: q.text "RGB" q.breakable q.group 2, "[", "]" do q.text html end if names q.breakable q.group 2, "{", "}" do last = names.last names.each { q.text _1 q.fill_breakable unless _1 == last } end end end ## def to_a = [red, green, blue] # :nodoc: ## alias_method :deconstruct, :to_a # :nodoc: ## def deconstruct_keys(_keys) = {r:, g:, b:, red:, green:, blue:} # :nodoc: ## def to_internal = [r, g, b] # :nodoc: ## # Outputs how much contrast this color has with another RGB color. # # The `delta_e94` algorithm uses ΔE*94 for contrast calculations and the `delta_e2000` # algorithm uses ΔE*2000. # # The `naive` algorithm treats the foreground and background colors as the same. # Any result over about 0.22 should have a high likelihood of being legible, but the # larger the difference, the more contrast. Otherwise, to be safe go with something # > 0.3. # # :call-seq: # contrast(other, algorithm: :naive) # contrast(other, algorithm: :delta_e94) # contrast(other, algorithm: :delta_e2000) def contrast(other, *args, **kwargs) other = coerce(other) algorithm = kwargs[:algorithm] || args.first || :naive case algorithm when :delta_e94 delta_e94(other) when :delta_e2000 delta_e2000(other) when :naive # The following numbers have been set with some care. ((diff_brightness(other) * 0.65) + (diff_hue(other) * 0.20) + (diff_luminosity(other) * 0.15)) else raise ARgumentError, "Unknown algorithm #{algorithm.inspect}" end end private ## def percent_adjustment(percent) # :nodoc: percent /= 100.0 percent += 1.0 percent = [percent, 2.0].min [0.0, percent].max end ## # Provides the luminosity difference between two rbg vals def diff_luminosity(other) # :nodoc: l1 = (0.2126 * other.r**2.2) + (0.7152 * other.b**2.2) + (0.0722 * other.g**2.2) l2 = (0.2126 * r**2.2) + (0.7152 * b**2.2) + (0.0722 * g**2.2) (([l1, l2].max + 0.05) / ([l1, l2].min + 0.05) - 1) / 20.0 end ## # Provides the brightness difference. def diff_brightness(other) # :nodoc: br1 = (299 * other.r + 587 * other.g + 114 * other.b) br2 = (299 * r + 587 * g + 114 * b) (br1 - br2).abs / 1000.0 end ## # Provides the euclidean distance between the two color values def diff_euclidean(other) ((((other.r - r)**2) + ((other.g - g)**2) + ((other.b - b)**2))**0.5) / 1.7320508075688772 end ## # Difference in the two colors' hue def diff_hue(other) # :nodoc: ((r - other.r).abs + (g - other.g).abs + (b - other.b).abs) / 3 end end class << Color::RGB ## # Creates a RGB color object from percentage values (0.0 .. 100.0). # # ```ruby # Color::RGB.from_percentage(10, 20, 30) # ``` def from_percentage(*args, **kwargs) r, g, b, names = case [args, kwargs] in [[r, g, b], {}] [r, g, b, nil] in [[_, _, _, _], {}] args in [[], {r:, g:, b:}] [r, g, b, nil] in [[], {r:, g:, b:, names:}] [r, g, b, names] else new(*args, **kwargs) end new(r: r / 100.0, g: g / 100.0, b: b / 100.0, names: names) end # Creates a RGB color object from the standard three byte range (0 .. 255). # # ```ruby # Color::RGB.from_values(32, 64, 128) # Color::RGB.from_values(0x20, 0x40, 0x80) # ``` def from_values(*args, **kwargs) r, g, b, names = case [args, kwargs] in [[r, g, b], {}] [r, g, b, nil] in [[_, _, _, _], {}] args in [[], {r:, g:, b:}] [r, g, b, nil] in [[], {r:, g:, b:, names:}] [r, g, b, names] else new(*args, **kwargs) end new(r: r / 255.0, g: g / 255.0, b: b / 255.0, names: names) end ## alias_method :from_fraction, :new ## alias_method :from_internal, :new # :nodoc: ## # Creates a RGB color object from an HTML color descriptor (e.g., `"fed"` or # `"#cabbed;"`. # # ```ruby # Color::RGB.from_html("fed") # Color::RGB.from_html("#fed") # Color::RGB.from_html("#cabbed") # Color::RGB.from_html("cabbed") # ``` def from_html(html_color) h = html_color.scan(/\h/i) r, g, b = case h.size when 3 h.map { |v| (v * 2).to_i(16) } when 6 h.each_slice(2).map { |v| v.join.to_i(16) } else raise ArgumentError, "Not a supported HTML color type." end from_values(r, g, b) end ## # Find or create a color by an HTML hex code. This differs from the #from_html method # in that if the color code matches a named color, the existing color will be # returned. # # ```ruby # Color::RGB.by_hex('ff0000').name # => 'red' # Color::RGB.by_hex('ff0001').name # => nil # ``` # # An exception will be raised if the value provided is not found or cannot be # interpreted as a valid hex colour. def by_hex(hex) = __by_hex.fetch(html_hexify(hex)) { from_html(hex) } ## # Return a color as identified by the color name. def by_name(name, &block) = __by_name.fetch(name.to_s.downcase, &block) ## # Return a color as identified by the color name, or by hex. def by_css(name_or_hex, &block) = by_name(name_or_hex) { by_hex(name_or_hex, &block) } ## # Extract named or hex colors from the provided text. def extract_colors(text, mode = :both) require "color/rgb/colors" text = text.downcase regex = case mode when :name Regexp.union(__by_name.keys) when :hex Regexp.union(__by_hex.keys) when :both Regexp.union(__by_hex.keys + __by_name.keys) else raise ArgumentError, "Unknown mode #{mode}" end text.scan(regex).map { |match| case mode when :name by_name(match) when :hex by_hex(match) when :both by_css(match) end } end private ## def __by_hex # :nodoc: require "color/rgb/colors" @__by_hex end ## def __by_name # :nodoc: require "color/rgb/colors" @__by_name end ## def html_hexify(hex) # :nodoc: h = hex.to_s.downcase.scan(/\h/) case h.size when 3 h.map { |v| (v * 2) }.join when 6 h.join else raise ArgumentError, "Not a supported HTML color type." end end end color-2.2.0/lib/color/rgb/0000755000004100000410000000000015137215736015331 5ustar www-datawww-datacolor-2.2.0/lib/color/rgb/colors.rb0000644000004100000410000002662515137215736017172 0ustar www-datawww-data# frozen_string_literal: true class Color::RGB # :stopdoc: class << self def __create_named_colors(mod, *colors) @__by_hex ||= {} @__by_name ||= {} colors.each do |color| color => {rgb:, names:} raise ArgumentError, "Names cannot be empty" if names.nil? || names.empty? used = names - mod.constants.map(&:to_sym) if used.length < names.length raise ArgumentError, "#{names.join(", ")} already defined in #{mod}" end rgb = rgb.with(names: Array(names).flatten.compact.map { _1.to_s.downcase }.sort.uniq) names.each { mod.const_set(_1, rgb) } rgb.names.each { @__by_name[_1] = @__by_name[_1.to_s] = rgb } lower = rgb.name.downcase @__by_name[lower] = @__by_name[lower.to_s] = rgb @__by_hex[rgb.hex] = rgb end end end __create_named_colors( self, {rgb: from_values(0xf0, 0xf8, 0xff), names: [:AliceBlue]}, {rgb: from_values(0xfa, 0xeb, 0xd7), names: [:AntiqueWhite]}, {rgb: from_values(0x00, 0xff, 0xff), names: [:Aqua]}, {rgb: from_values(0x7f, 0xff, 0xd4), names: [:Aquamarine]}, {rgb: from_values(0xf0, 0xff, 0xff), names: [:Azure]}, {rgb: from_values(0xf5, 0xf5, 0xdc), names: [:Beige]}, {rgb: from_values(0xff, 0xe4, 0xc4), names: [:Bisque]}, {rgb: from_values(0x00, 0x00, 0x00), names: [:Black]}, {rgb: from_values(0xff, 0xeb, 0xcd), names: [:BlanchedAlmond]}, {rgb: from_values(0x00, 0x00, 0xff), names: [:Blue]}, {rgb: from_values(0x8a, 0x2b, 0xe2), names: [:BlueViolet]}, {rgb: from_values(0xa5, 0x2a, 0x2a), names: [:Brown]}, {rgb: from_values(0xde, 0xb8, 0x87), names: [:BurlyWood, :Burlywood]}, {rgb: from_values(0x5f, 0x9e, 0xa0), names: [:CadetBlue]}, {rgb: from_values(0xff, 0x5e, 0xd0), names: [:Carnation]}, {rgb: from_values(0x8d, 0x00, 0x00), names: [:Cayenne]}, {rgb: from_values(0x7f, 0xff, 0x00), names: [:Chartreuse]}, {rgb: from_values(0xd2, 0x69, 0x1e), names: [:Chocolate]}, {rgb: from_values(0xff, 0x7f, 0x50), names: [:Coral]}, {rgb: from_values(0x64, 0x95, 0xed), names: [:CornflowerBlue]}, {rgb: from_values(0xff, 0xf8, 0xdc), names: [:Cornsilk]}, {rgb: from_values(0xdc, 0x14, 0x3c), names: [:Crimson]}, {rgb: from_values(0x00, 0xff, 0xff), names: [:Cyan]}, {rgb: from_values(0x00, 0x00, 0x8b), names: [:DarkBlue]}, {rgb: from_values(0x00, 0x8b, 0x8b), names: [:DarkCyan]}, {rgb: from_values(0xb8, 0x86, 0x0b), names: [:DarkGoldenRod, :DarkGoldenrod]}, {rgb: from_values(0xa9, 0xa9, 0xa9), names: [:DarkGray, :DarkGrey]}, {rgb: from_values(0x00, 0x64, 0x00), names: [:DarkGreen]}, {rgb: from_values(0xbd, 0xb7, 0x6b), names: [:DarkKhaki]}, {rgb: from_values(0x8b, 0x00, 0x8b), names: [:DarkMagenta]}, {rgb: from_values(0x55, 0x6b, 0x2f), names: [:DarkOliveGreen, :DarkoliveGreen]}, {rgb: from_values(0xff, 0x8c, 0x00), names: [:DarkOrange, :Darkorange]}, {rgb: from_values(0x99, 0x32, 0xcc), names: [:DarkOrchid]}, {rgb: from_values(0x8b, 0x00, 0x00), names: [:DarkRed]}, {rgb: from_values(0xe9, 0x96, 0x7a), names: [:DarkSalmon, :Darksalmon]}, {rgb: from_values(0x8f, 0xbc, 0x8f), names: [:DarkSeaGreen]}, {rgb: from_values(0x48, 0x3d, 0x8b), names: [:DarkSlateBlue]}, {rgb: from_values(0x2f, 0x4f, 0x4f), names: [:DarkSlateGray, :DarkSlateGrey]}, {rgb: from_values(0x00, 0xce, 0xd1), names: [:DarkTurquoise]}, {rgb: from_values(0x94, 0x00, 0xd3), names: [:DarkViolet]}, {rgb: from_values(0xff, 0x14, 0x93), names: [:DeepPink]}, {rgb: from_values(0x00, 0xbf, 0xff), names: [:DeepSkyBlue]}, {rgb: from_values(0x69, 0x69, 0x69), names: [:DimGray, :DimGrey]}, {rgb: from_values(0x1e, 0x90, 0xff), names: [:DodgerBlue]}, {rgb: from_values(0xd1, 0x92, 0x75), names: [:Feldspar]}, {rgb: from_values(0xb2, 0x22, 0x22), names: [:FireBrick, :Firebrick]}, {rgb: from_values(0xff, 0xfa, 0xf0), names: [:FloralWhite]}, {rgb: from_values(0x22, 0x8b, 0x22), names: [:ForestGreen]}, {rgb: from_values(0xff, 0x00, 0xff), names: [:Fuchsia]}, {rgb: from_values(0xdc, 0xdc, 0xdc), names: [:Gainsboro]}, {rgb: from_values(0xf8, 0xf8, 0xff), names: [:GhostWhite]}, {rgb: from_values(0xff, 0xd7, 0x00), names: [:Gold]}, {rgb: from_values(0xda, 0xa5, 0x20), names: [:GoldenRod, :Goldenrod]}, {rgb: from_values(0x80, 0x80, 0x80), names: [:Gray, :Grey]}, {rgb: from_fraction(0.05, 0.05, 0.05), names: [:Gray05, :Gray5, :Grey05, :Grey5]}, *(10..95).step(5).map { v = _1 / 100.0 {rgb: from_fraction(v, v, v), names: [:"Gray#{_1}", :"Grey#{_1}"]} }, {rgb: from_values(0x00, 0x80, 0x00), names: [:Green]}, {rgb: from_values(0xad, 0xff, 0x2f), names: [:GreenYellow]}, {rgb: from_values(0xf0, 0xff, 0xf0), names: [:HoneyDew, :Honeydew]}, {rgb: from_values(0xff, 0x69, 0xb4), names: [:HotPink]}, {rgb: from_values(0xcd, 0x5c, 0x5c), names: [:IndianRed]}, {rgb: from_values(0x4b, 0x00, 0x82), names: [:Indigo]}, {rgb: from_values(0xff, 0xff, 0xf0), names: [:Ivory]}, {rgb: from_values(0xf0, 0xe6, 0x8c), names: [:Khaki]}, {rgb: from_values(0xe6, 0xe6, 0xfa), names: [:Lavender]}, {rgb: from_values(0xff, 0xf0, 0xf5), names: [:LavenderBlush]}, {rgb: from_values(0x7c, 0xfc, 0x00), names: [:LawnGreen]}, {rgb: from_values(0xff, 0xfa, 0xcd), names: [:LemonChiffon]}, {rgb: from_values(0xad, 0xd8, 0xe6), names: [:LightBlue]}, {rgb: from_values(0xf0, 0x80, 0x80), names: [:LightCoral]}, {rgb: from_values(0xe0, 0xff, 0xff), names: [:LightCyan]}, {rgb: from_values(0xfa, 0xfa, 0xd2), names: [:LightGoldenRodYellow, :LightGoldenrodYellow]}, {rgb: from_values(0xd3, 0xd3, 0xd3), names: [:LightGray, :LightGrey]}, {rgb: from_values(0x90, 0xee, 0x90), names: [:LightGreen]}, {rgb: from_values(0xff, 0xb6, 0xc1), names: [:LightPink]}, {rgb: from_values(0xff, 0xa0, 0x7a), names: [:LightSalmon, :Lightsalmon]}, {rgb: from_values(0x20, 0xb2, 0xaa), names: [:LightSeaGreen]}, {rgb: from_values(0x87, 0xce, 0xfa), names: [:LightSkyBlue]}, {rgb: from_values(0x84, 0x70, 0xff), names: [:LightSlateBlue]}, {rgb: from_values(0x77, 0x88, 0x99), names: [:LightSlateGray, :LightSlateGrey]}, {rgb: from_values(0xb0, 0xc4, 0xde), names: [:LightSteelBlue, :LightsteelBlue]}, {rgb: from_values(0xff, 0xff, 0xe0), names: [:LightYellow]}, {rgb: from_values(0x00, 0xff, 0x00), names: [:Lime]}, {rgb: from_values(0x32, 0xcd, 0x32), names: [:LimeGreen]}, {rgb: from_values(0xfa, 0xf0, 0xe6), names: [:Linen]}, {rgb: from_values(0xff, 0x00, 0xff), names: [:Magenta]}, {rgb: from_values(0x80, 0x00, 0x00), names: [:Maroon]}, {rgb: from_values(0x66, 0xcd, 0xaa), names: [:MediumAquaMarine, :MediumAquamarine]}, {rgb: from_values(0x00, 0x00, 0xcd), names: [:MediumBlue]}, {rgb: from_values(0xba, 0x55, 0xd3), names: [:MediumOrchid]}, {rgb: from_values(0x93, 0x70, 0xdb), names: [:MediumPurple]}, {rgb: from_values(0x3c, 0xb3, 0x71), names: [:MediumSeaGreen]}, {rgb: from_values(0x7b, 0x68, 0xee), names: [:MediumSlateBlue]}, {rgb: from_values(0x00, 0xfa, 0x9a), names: [:MediumSpringGreen]}, {rgb: from_values(0x48, 0xd1, 0xcc), names: [:MediumTurquoise]}, {rgb: from_values(0xc7, 0x15, 0x85), names: [:MediumVioletRed]}, {rgb: from_values(0x19, 0x19, 0x70), names: [:MidnightBlue]}, {rgb: from_values(0xf5, 0xff, 0xfa), names: [:MintCream]}, {rgb: from_values(0xff, 0xe4, 0xe1), names: [:MistyRose]}, {rgb: from_values(0xff, 0xe4, 0xb5), names: [:Moccasin]}, {rgb: from_values(0xff, 0xde, 0xad), names: [:NavajoWhite]}, {rgb: from_values(0x00, 0x00, 0x80), names: [:Navy]}, {rgb: from_values(0xfd, 0xf5, 0xe6), names: [:OldLace]}, {rgb: from_values(0x80, 0x80, 0x00), names: [:Olive]}, {rgb: from_values(0x6b, 0x8e, 0x23), names: [:OliveDrab, :Olivedrab]}, {rgb: from_values(0xff, 0xa5, 0x00), names: [:Orange]}, {rgb: from_values(0xff, 0x45, 0x00), names: [:OrangeRed]}, {rgb: from_values(0xda, 0x70, 0xd6), names: [:Orchid]}, {rgb: from_values(0xee, 0xe8, 0xaa), names: [:PaleGoldenRod, :PaleGoldenrod]}, {rgb: from_values(0x98, 0xfb, 0x98), names: [:PaleGreen]}, {rgb: from_values(0xaf, 0xee, 0xee), names: [:PaleTurquoise]}, {rgb: from_values(0xdb, 0x70, 0x93), names: [:PaleVioletRed]}, {rgb: from_values(0xff, 0xef, 0xd5), names: [:PapayaWhip]}, {rgb: from_values(0xff, 0xda, 0xb9), names: [:PeachPuff, :Peachpuff]}, {rgb: from_values(0xcd, 0x85, 0x3f), names: [:Peru]}, {rgb: from_values(0xff, 0xc0, 0xcb), names: [:Pink]}, {rgb: from_values(0xdd, 0xa0, 0xdd), names: [:Plum]}, {rgb: from_values(0xb0, 0xe0, 0xe6), names: [:PowderBlue]}, {rgb: from_values(0x80, 0x00, 0x80), names: [:Purple]}, {rgb: from_values(0x66, 0x33, 0x99), names: [:RebeccaPurple]}, {rgb: from_values(0xff, 0x00, 0x00), names: [:Red]}, {rgb: from_values(0xbc, 0x8f, 0x8f), names: [:RosyBrown]}, {rgb: from_values(0x41, 0x69, 0xe1), names: [:RoyalBlue]}, {rgb: from_values(0x8b, 0x45, 0x13), names: [:SaddleBrown]}, {rgb: from_values(0xfa, 0x80, 0x72), names: [:Salmon]}, {rgb: from_values(0xf4, 0xa4, 0x60), names: [:SandyBrown]}, {rgb: from_values(0x2e, 0x8b, 0x57), names: [:SeaGreen]}, {rgb: from_values(0xff, 0xf5, 0xee), names: [:SeaShell, :Seashell]}, {rgb: from_values(0xa0, 0x52, 0x2d), names: [:Sienna]}, {rgb: from_values(0xc0, 0xc0, 0xc0), names: [:Silver]}, {rgb: from_values(0x87, 0xce, 0xeb), names: [:SkyBlue]}, {rgb: from_values(0x6a, 0x5a, 0xcd), names: [:SlateBlue]}, {rgb: from_values(0x70, 0x80, 0x90), names: [:SlateGray, :SlateGrey]}, {rgb: from_values(0xff, 0xfa, 0xfa), names: [:Snow]}, {rgb: from_values(0x00, 0xff, 0x7f), names: [:SpringGreen]}, {rgb: from_values(0x46, 0x82, 0xb4), names: [:SteelBlue]}, {rgb: from_values(0xd2, 0xb4, 0x8c), names: [:Tan]}, {rgb: from_values(0x00, 0x80, 0x80), names: [:Teal]}, {rgb: from_values(0xd8, 0xbf, 0xd8), names: [:Thistle]}, {rgb: from_values(0xff, 0x63, 0x47), names: [:Tomato]}, {rgb: from_values(0x40, 0xe0, 0xd0), names: [:Turquoise]}, {rgb: from_values(0xee, 0x82, 0xee), names: [:Violet]}, {rgb: from_values(0xd0, 0x20, 0x90), names: [:VioletRed]}, {rgb: from_values(0xf5, 0xde, 0xb3), names: [:Wheat]}, {rgb: from_values(0xff, 0xff, 0xff), names: [:White]}, {rgb: from_values(0xf5, 0xf5, 0xf5), names: [:WhiteSmoke]}, {rgb: from_values(0xff, 0xff, 0x00), names: [:Yellow]}, {rgb: from_values(0x9a, 0xcd, 0x32), names: [:YellowGreen]} ) # :stopdoc: # This namespace contains some RGB metallic colors suggested by Jim # Freeze. # :startdoc: module Metallic; end __create_named_colors( Metallic, {rgb: from_values(0x99, 0x99, 0x99), names: [:Aluminum]}, {rgb: from_values(0xd9, 0x87, 0x19), names: [:CoolCopper]}, {rgb: from_values(0xb8, 0x73, 0x33), names: [:Copper]}, {rgb: from_values(0x4c, 0x4c, 0x4c), names: [:Iron]}, {rgb: from_values(0x19, 0x19, 0x19), names: [:Lead]}, {rgb: from_values(0xb3, 0xb3, 0xb3), names: [:Magnesium]}, {rgb: from_values(0xe6, 0xe6, 0xe6), names: [:Mercury]}, {rgb: from_values(0x80, 0x80, 0x80), names: [:Nickel]}, {rgb: from_values(0x60, 0x00, 0x00), names: [:PolySilicon, :Poly]}, {rgb: from_values(0xcc, 0xcc, 0xcc), names: [:Silver]}, {rgb: from_values(0x66, 0x66, 0x66), names: [:Steel]}, {rgb: from_values(0x7f, 0x7f, 0x7f), names: [:Tin]}, {rgb: from_values(0x33, 0x33, 0x33), names: [:Tungsten]} ) class << self undef :__create_named_colors end # :startdoc: end color-2.2.0/lib/color/hsl.rb0000644000004100000410000001664415137215736015705 0ustar www-datawww-data# frozen_string_literal: true ## # The \HSL color model is a cylindrical-coordinate representation of the sRGB color model, # standing for hue (measured in degrees on the cylinder), saturation (measured in # percentage), and lightness (measured in percentage). # # \HSL colors are immutable Data class instances. Array deconstruction is `[hue, # saturation, luminosity]` and hash deconstruction is `{h:, hue:, s:, saturation:, l:, # luminosity:}`. See #h, #hue, #s, #saturation, #l, #luminosity. class Color::HSL include Color ## # :attr_reader: h # Returns the hue of the color in the range 0.0..1.0. ## # :attr_reader: hue # Returns the hue of the color in degrees (0.0..360.0). ## # :attr_reader: s # Returns the saturation of the color in the range 0.0..1.0. ## # :attr_reader: saturation # Returns the percentage of saturation of the color (0.0..100.0). ## # :attr_reader: brightness # Returns the luminosity (#l) of the color. ## # :attr_reader: l # Returns the luminosity of the color in the range 0.0..1.0. ## # :attr_reader: luminosity # Returns the percentage of luminosity of the color. ## # Creates a \HSL color object from degrees (0.0 .. 360.0) and percentage values # (0.0 .. 100.0). # # ```ruby # Color::HSL.from_values(145, 30, 50) # => HSL [145deg 30% 50%] # Color::HSL.from_values(h: 145, s: 30, l: 50) # => HSL [145deg 30% 50%] # ``` # # :call-seq: # from_values(h, s, l) # from_values(h:, s:, l:) def self.from_values(*args, **kwargs) h, s, l = case [args, kwargs] in [[_, _, _], {}] args in [[], {h:, s:, l:}] [h, s, l] else new(*args, **kwargs) end new(h: h / 360.0, s: s / 100.0, l: l / 100.0) end class << self alias_method :from_fraction, :new alias_method :from_internal, :new # :nodoc: end ## # Creates a \HSL color object from fractional values (0.0 .. 1.0). # # ```ruby # Color::HSL.from_fraction(0.3, 0.3, 0.5) # => HSL [108deg 30% 50%] # Color::HSL.new(h: 0.3, s: 0.3, l: 0.5) # => HSL [108deg 30% 50%] # Color::HSL[0.3, 0.3, 0.5] # => HSL [108deg 30% 50%] # ``` def initialize(h:, s:, l:) super(h: normalize(h), s: normalize(s), l: normalize(l)) end ## # Coerces the other Color object into \HSL. def coerce(other) = other.to_hsl ## # Converts from \HSL to Color::RGB. # # As with all color conversions, this is an approximation. The code here is adapted from # fvd and van Dam, originally found at [1] (implemented similarly at [2]). This # simplifies the calculations with the following assumptions: # # - Luminance values <= 0 always translate to a black Color::RGB value. # - Luminance values >= 1 always translate to a white Color::RGB value. # - Saturation values <= 0 always translate to a shade of gray using luminance as # a percentage of gray. # # [1] https://web.archive.org/web/20150311023529/http://bobpowell.net/RGBHSB.aspx # [2] https://support.microsoft.com/kb/29240 def to_rgb(...) if near_zero_or_less?(l) Color::RGB::Black000 elsif near_one_or_more?(l) Color::RGB::WhiteFFF elsif near_zero?(s) Color::RGB.from_fraction(l, l, l) else Color::RGB.from_fraction(*compute_fvd_rgb) end end ## # Converts from \HSL to Color::YIQ via Color::RGB. def to_yiq(...) = to_rgb(...).to_yiq(...) ## # Converts from \HSL to Color::CMYK via Color::RGB. def to_cmyk(...) = to_rgb(...).to_cmyk(...) ## # Converts from \HSL to Color::Grayscale. # # Luminance is treated as the Grayscale ratio. def to_grayscale(...) = Color::Grayscale.from_fraction(l) ## # Converts from \HSL to Color::CIELAB via Color::RGB. def to_lab(...) = to_rgb(...).to_lab(...) ## # Converts from \HSL to Color::XYZ via Color::RGB. def to_xyz(...) = to_rgb(...).to_xyz(...) ## def to_hsl(...) = self ## # Present the color as a CSS `hsl` function with optional `alpha`. # # ```ruby # hsl = Color::HSL.from_values(145, 30, 50) # hsl.css # => hsl(145deg 30% 50%) # hsl.css(alpha: 0.5) # => hsl(145deg 30% 50% / 0.50) # ``` def css(alpha: nil) params = [ css_value(hue, :degrees), css_value(saturation, :percent), css_value(luminosity, :percent) ].join(" ") params = "#{params} / #{css_value(alpha)}" if alpha "hsl(#{params})" end ## def brightness = l # :nodoc: ## def hue = h * 360.0 # :nodoc: ## def saturation = s * 100.0 # :nodoc: ## def luminosity = l * 100.0 # :nodoc: ## alias_method :lightness, :luminosity ## def inspect = "HSL [%.2fdeg %.2f%% %.2f%%]" % [hue, saturation, luminosity] # :nodoc: ## def pretty_print(q) # :nodoc: q.text "HSL" q.breakable q.group 2, "[", "]" do q.text "%.2fdeg" % hue q.fill_breakable q.text "%.2f%%" % saturation q.fill_breakable q.text "%.2f%%" % luminosity end end ## # Mix the mask color (which will be converted to a \HSL color) with the current color # at the stated fractional mix ratio (0.0..1.0). # # This implementation differs from Color::RGB#mix_with. # # :call-seq: # mix_with(mask, mix_ratio: 0.5) def mix_with(mask, *args, **kwargs) mix_ratio = normalize(kwargs[:mix_ratio] || args.first || 0.5) map_with(mask) { ((_2 - _1) * mix_ratio) + _1 } end ## def to_a = [hue, saturation, luminosity] # :nodoc: ## alias_method :deconstruct, :to_a ## def deconstruct_keys(_keys) = {h:, s:, l:, hue:, saturation:, luminance:} # :nodoc: ## def to_internal = [h, s, l] # :nodoc: private ## # This algorithm calculates based on a mixture of the saturation and luminance, and then # takes the RGB values from the hue + 1/3, hue, and hue - 1/3 positions in a circular # representation of color divided into four parts (confusing, I know, but it's the way # that it works). See #hue_to_rgb for more information. def compute_fvd_rgb # :nodoc: t1, t2 = fvd_mix_sat_lum [h + (1 / 3.0), h, h - (1 / 3.0)].map { |v| hue_to_rgb(rotate_hue(v), t1, t2) } end ## # Mix saturation and luminance for use in hue_to_rgb. The base value is different # depending on whether luminance is <= 50% or > 50%. def fvd_mix_sat_lum # :nodoc: t = if near_zero_or_less?(l - 0.5) l * (1.0 + s.to_f) else l + s - (l * s.to_f) end [2.0 * l - t, t] end ## # In \HSL, hues are referenced as degrees in a color circle. The flow itself is endless; # therefore, we can rotate around. The only thing our implementation restricts is that # you should not be > 1.0. def rotate_hue(h) # :nodoc: h += 1.0 if near_zero_or_less?(h) h -= 1.0 if near_one_or_more?(h) h end ## # We calculate the interaction of the saturation/luminance mix (calculated earlier) # based on the position of the hue in the circular color space divided into quadrants. # Our hue range is [0, 1), not [0, 360º). # # - The first quadrant covers the first 60º [0, 60º]. # - The second quadrant covers the next 120º (60º, 180º]. # - The third quadrant covers the next 60º (180º, 240º]. # - The fourth quadrant covers the final 120º (240º, 360º). def hue_to_rgb(h, t1, t2) # :nodoc: if near_zero_or_less?((6.0 * h) - 1.0) t1 + ((t2 - t1) * h * 6.0) elsif near_zero_or_less?((2.0 * h) - 1.0) t2 elsif near_zero_or_less?((3.0 * h) - 2.0) t1 + (t2 - t1) * ((2 / 3.0) - h) * 6.0 else t1 end end end color-2.2.0/lib/color/version.rb0000644000004100000410000000011615137215736016567 0ustar www-datawww-data# frozen_string_literal: true module Color VERSION = "2.2.0" # :nodoc: end color-2.2.0/lib/color/cmyk.rb0000644000004100000410000002332315137215736016052 0ustar www-datawww-data# frozen_string_literal: true ## # The \CMYK color model is a subtractive color model based on additive percentages of # colored inks: cyan, magenta, yellow, and key (most often black). # # \CMYK [30% 0% 80% 30%] would be mixed from 30% cyan, 0% magenta, 80% yellow, and 30% # black. # # \CMYK colors are immutable Data class instances. Array deconstruction is `[cyan, # magenta, yellow, key]` and hash deconstruction is `{c:, cyan:, m:, magenta:, y:, yellow: # k:, key:}`. See #c, #cyan, #m, #magenta, #y, #yellow, #k, #key. class Color::CMYK include Color ## # :attr_reader: c # Returns the cyan (`C`) component as a value 0.0 .. 1.0. ## # :attr_reader: cyan # Returns the cyan (`C`) component as a percentage value (0.0 .. 100.0). ## # :attr_reader: m # Returns the magenta (`M`) component as a value 0.0 .. 1.0. ## # :attr_reader: magenta # Returns the magenta (`M`) component as a percentage value (0.0 .. 100.0). ## # :attr_reader: y # Returns the yellow (`Y`) component as a value 0.0 .. 1.0. ## # :attr_reader: yellow # Returns the yellow (`Y`) component as a percentage value (0.0 .. 100.0). ## # :attr_reader: k # Returns the key or black (`K`) component as a value 0.0 .. 1.0. ## # :attr_reader: b # Returns the key or black (`K`) component as a value 0.0 .. 1.0. ## # :attr_reader: key # Returns the key or black (`K`) component as a percentage value (0.0 .. 100.0). ## # :attr_reader: black # Returns the key or black (`K`) component as a percentage value (0.0 .. 100.0). ## # Creates a CMYK color object from percentage values (0.0 .. 100.0). # # ```ruby # Color::CMYK.from_percentage(30, 0, 80, 30) # => CMYK [30.00% 0.00% 80.00% 30.00%] # Color::CMYK.from_values(30, 0, 80, 30) # => CMYK [30.00% 0.00% 80.00% 30.00%] # ``` # # :call-seq: # from_percentage(c, m, y, k) # from_percentage(c:, m:, y:, k:) # from_values(c, m, y, k) # from_values(c:, m:, y:, k:) def self.from_percentage(*args, **kwargs) c, m, y, k = case [args, kwargs] in [[_, _, _, _], {}] args in [[], {c:, m:, y:, k:}] [c, m, y, k] else new(*args, **kwargs) end new(c: c / 100.0, m: m / 100.0, y: y / 100.0, k: k / 100.0) end class << self alias_method :from_values, :from_percentage alias_method :from_fraction, :new alias_method :from_internal, :new # :nodoc: end ## # Creates a CMYK color object from fractional values (0.0 .. 1.0). # # ```ruby # Color::CMYK.from_fraction(0.3, 0, 0.8, 0.3) # => CMYK [30.00% 0.00% 80.00% 30.00%] # Color::CMYK.new(0.3, 0, 0.8, 0.3) # => CMYK [30.00% 0.00% 80.00% 30.00%] # Color::CMYK[c: 0.3, m: 0, y: 0.8, k: 0.3] # => CMYK [30.00% 0.00% 80.00% 30.00%] # ``` def initialize(c:, m:, y:, k:) super(c: normalize(c), m: normalize(m), y: normalize(y), k: normalize(k)) end ## # Output a CSS representation of the CMYK color using `device-cmyk()`. # # If an `alpha` value is provided, it will be included in the output. # # A `fallback` may be provided or included automatically depending on the value # provided, which may be `true`, `false`, a Color object, or a Hash with `:color` and/or # `:alpha` keys. The default value is `true`. # # When `fallback` is: # # - `true`: this CMYK color will be converted to RGB and this will be provided as the # fallback color. If an `alpha` value is provided, it will be used for the fallback. # - `false`: no fallback color will be included. # - a Color object will be used to produce the `fallback` value. # - a Hash will be checked for `:color` and/or `:alpha` keys: # - if `:color` is present, it will be used for the fallback color; if not present, # the CMYK color will be converted to RGB. # - if `:alpha` is present, it will be used for the fallback color; if not present, # the fallback color will be presented _without_ alpha. # # Examples: # # ```ruby # cmyk = Color::CMYK.from_percentage(30, 0, 80, 30) # cmyk.css # # => device-cmyk(30.00% 0 80.00% 30.00%, rgb(49.00% 70.00% 14.00%)) # # cmyk.css(alpha: 0.5) # # => device-cmyk(30.00% 0 80.00% 30.00% / 0.50, rgb(49.00% 70.00% 14.00% / 0.50)) # # cmyk.css(fallback: false) # # => device-cmyk(30.00% 0 80.00% 30.00%) # # cmyk.css(alpha: 0.5, fallback: false) # # => device-cmyk(30.00% 0 80.00% 30.00% / 0.50) # # cmyk.css(fallback: Color::RGB::Blue) # # => device-cmyk(30.00% 0 80.00% 30.00%, rgb(0.00% 0.00% 100.00%)) # # cmyk.css(alpha: 0.5, fallback: Color::RGB::Blue) # # => device-cmyk(30.00% 0 80.00% 30.00% / 0.50, rgb(0.00% 0.00% 100.00% / 0.50)) # # cmyk.css(alpha: 0.5, fallback: { color: Color::RGB::Blue }) # # => device-cmyk(30.00% 0 80.00% 30.00% / 0.50, rgb(0.00% 0.00% 100.00%)) # # cmyk.css(alpha: 0.5, fallback: { color: Color::RGB::Blue, alpha: 0.3 }) # # => device-cmyk(30.00% 0 80.00% 30.00% / 0.50, rgb(0.00% 0.00% 100.00% / 0.30)) # # cmyk.css(alpha: 0.5, fallback: { alpha: 0.3 }) # # => device-cmyk(30.00% 0 80.00% 30.00% / 0.50, rgb(49.00% 70.00% 14.00% / 0.30)) # ``` def css(alpha: nil, fallback: true) if fallback.is_a?(Color) device_cmyk(alpha, fallback, alpha) elsif fallback.is_a?(Hash) device_cmyk(alpha, fallback.fetch(:color) { to_rgb }, fallback[:alpha]) elsif fallback == true device_cmyk(alpha, to_rgb, alpha) else device_cmyk(alpha, nil, nil) end end ## # Coerces the other Color object into CMYK. def coerce(other) = other.to_cmyk ## def to_cmyk(...) = self ## # Converts CMYK to Color::Grayscale. # # There are multiple methods for grayscale conversion, but this implements a variant of # the Adobe PDF conversion method with higher precision: # # ``` # g = 1.0 - min(1.0, 0.299 * c + 0.587 * m + 0.114 * y + k) # ``` # # The default Adobe conversion uses lower precision conversion constants (0.3, 0.59, and # 0.11) instead of the more precise NTSC/YIQ values. def to_grayscale(...) gc = 0.299 * c gm = 0.587 * m gy = 0.114 * y g = 1.0 - [1.0, gc + gm + gy + k].min Color::Grayscale.from_fraction(g) end ## # Converts CMYK to Color::YIQ via Color::RGB. def to_yiq(...) = to_rgb(...).to_yiq(...) ## # Converts CMYK to Color::RGB. # # Most color experts strongly suggest that this is not a good idea (some suggesting that # it's a very bad idea). CMYK represents additive percentages of inks on white paper, # whereas RGB represents mixed color intensities on an unlit (black) screen. # # The color conversion can be done and there are two different methods (standard and # Adobe PDF) that provide slightly different results. Using CMYK [33% 66% 83% 25%], the # standard method provides an approximate RGB color of (128, 65, 33) or #804121. The # Adobe PDF method provides an approximate RGB color of (107, 23, 0) or #6b1700. # # Which is correct? The colors may seem to be drastically different in the RGB color # space, they differ mostly in intensity. The Adobe PDF conversion is a darker, slightly # redder brown; the standard conversion is a lighter brown. Because of this subtlety, # both methods are offered for conversion. The Adobe PDF method is not used by default; # to use it, pass `rgb_method: :adobe` to #to_rgb. # # # Adobe PDF CMYK -> RGB Conversion # r = 1.0 - min(1.0, c + k) # g = 1.0 - min(1.0, m + k) # b = 1.0 - min(1.0, y + k) # # # Standard CMYK -> RGB Conversion # r = 1.0 - (c * (1.0 - k) + k) # g = 1.0 - (m * (1.0 - k) + k) # b = 1.0 - (y * (1.0 - k) + k) # # :call-seq: # to_rgb(rgb_method: :standard) def to_rgb(*args, **kwargs) values = if kwargs[:rgb_method] == :adobe || args.first == :adobe adobe_cmyk_rgb else standard_cmyk_rgb end Color::RGB.from_fraction(*values) end ## # Converts CMYK to Color::HSL via Color::RGB. def to_hsl(...) = to_rgb(...).to_hsl(...) ## # Converts CMYK to Color::CIELAB via Color::RGB. def to_lab(...) = to_rgb(...).to_lab(...) ## # Converts CMYK to Color::XYZ via Color::RGB. def to_xyz(...) = to_rgb(...).to_xyz(...) ## def inspect = "CMYK [%.2f%% %.2f%% %.2f%% %.2f%%]" % [cyan, magenta, yellow, key] # :nodoc: ## def pretty_print(q) # :nodoc: q.text "CMYK" q.breakable q.group 2, "[", "]" do q.text "%.2f%%" % cyan q.fill_breakable q.text "%.2f%%" % magenta q.fill_breakable q.text "%.2f%%" % yellow q.fill_breakable q.text "%.2f%%" % key end end ## def cyan = c * 100.0 # :nodoc: ## def magenta = m * 100.0 # :nodoc: ## def yellow = y * 100.0 # :nodoc: ## alias_method :b, :k # :nodoc: ## def key = k * 100.0 # :nodoc: ## alias_method :black, :key # :nodoc: ## def to_a = [cyan, magenta, yellow, key] # :nodoc: ## alias_method :deconstruct, :to_a # :nodoc: ## def deconstruct_keys(_keys) = {c:, m:, y:, k:, cyan:, magenta:, yellow:, key:} # :nodoc: ## def to_internal = [c, m, y, k] # :nodoc: ## def components = 4 # :nodoc: private ## # Implements the Adobe PDF conversion of CMYK to RGB. def adobe_cmyk_rgb = [c, m, y].map { 1.0 - [1.0, _1 + k].min } # :nodoc: ## # Implements the standard conversion of CMYK to RGB. def standard_cmyk_rgb = [c, m, y].map { 1.0 - (_1 * (1.0 - k) + k) } # :nodoc: ## def device_cmyk(alpha, fallback, fallback_alpha) # :nodoc: params = [ css_value(cyan, :percent), css_value(magenta, :percent), css_value(yellow, :percent), css_value(key, :percent) ].join(" ") params = "#{params} / #{css_value(alpha)}" if alpha fallback = fallback&.css(alpha: fallback_alpha) if fallback "device-cmyk(#{params}, #{fallback})" else "device-cmyk(#{params})" end end end color-2.2.0/test/0000755000004100000410000000000015137215736013652 5ustar www-datawww-datacolor-2.2.0/test/minitest_helper.rb0000644000004100000410000000120115137215736017364 0ustar www-datawww-data# frozen_string_literal: true require "color" require "color/rgb/colors" require "pp" gem "minitest" require "minitest/autorun" require "minitest/focus" if ENV["STRICT"] $VERBOSE = true Warning[:deprecated] = true require "minitest/error_on_warning" end module Minitest::ColorExtensions def assert_in_tolerance(expected, actual, msg = nil) assert_in_delta expected, actual, Color::TOLERANCE, msg end def assert_pretty_inspect(expected, object, msg = nil) actual = PP.pp(object, +"", 8) assert_equal expected, actual, message(msg, nil) { diff expected, actual } end Minitest::Test.send(:include, self) end color-2.2.0/test/test_color.rb0000644000004100000410000000567315137215736016367 0ustar www-datawww-data# frozen_string_literal: true require "color" require "minitest_helper" module TestColor class TestColor < Minitest::Test def setup @subject = Object.new.extend(Color) end # def test_equivalent # assert Color.equivalent?(Color::RGB::Red, Color::HSL.from_values(0, 100, 50)) # refute Color.equivalent?(Color::RGB::Red, nil) # refute Color.equivalent?(nil, Color::RGB::Red) # end def test_normalize normalize = @subject.method(:normalize) (1..10).each do |i| assert_equal(0.0, normalize.call(-7 * i)) assert_equal(0.0, normalize.call(-7 / i)) assert_equal(0.0, normalize.call(0 - i)) assert_equal(1.0, normalize.call(255 + i)) assert_equal(1.0, normalize.call(256 * i)) assert_equal(1.0, normalize.call(65536 / i)) end (0..255).each do |i| assert_in_delta(i / 255.0, normalize.call(i / 255.0), 1e-2) end end def test_normalize_byte normalize_byte = @subject.method(:normalize_byte) assert_equal(0, normalize_byte.call(-1)) assert_equal(0, normalize_byte.call(0)) assert_equal(127, normalize_byte.call(127)) assert_equal(172, normalize_byte.call(172)) assert_equal(255, normalize_byte.call(255)) assert_equal(255, normalize_byte.call(256)) end def test_normalize_word normalize_word = @subject.method(:normalize_word) assert_equal(0, normalize_word.call(-1)) assert_equal(0, normalize_word.call(0)) assert_equal(127, normalize_word.call(127)) assert_equal(172, normalize_word.call(172)) assert_equal(255, normalize_word.call(255)) assert_equal(256, normalize_word.call(256)) assert_equal(65535, normalize_word.call(65535)) assert_equal(65535, normalize_word.call(66536)) end def test_normalize_range normalize_to_range = @subject.method(:normalize_to_range) assert_equal(-100, normalize_to_range.call(-101, -100..100)) assert_equal(-100, normalize_to_range.call(-100.5, -100..100)) assert_equal(-100, normalize_to_range.call(-100, -100..100)) assert_equal(-100, normalize_to_range.call(-100.0, -100..100)) assert_equal(-99.5, normalize_to_range.call(-99.5, -100..100)) assert_equal(-50, normalize_to_range.call(-50, -100..100)) assert_equal(-50.5, normalize_to_range.call(-50.5, -100..100)) assert_equal(0, normalize_to_range.call(0, -100..100)) assert_equal(50, normalize_to_range.call(50, -100..100)) assert_equal(50.5, normalize_to_range.call(50.5, -100..100)) assert_equal(99, normalize_to_range.call(99, -100..100)) assert_equal(99.5, normalize_to_range.call(99.5, -100..100)) assert_equal(100, normalize_to_range.call(100, -100..100)) assert_equal(100, normalize_to_range.call(100.0, -100..100)) assert_equal(100, normalize_to_range.call(100.5, -100..100)) assert_equal(100, normalize_to_range.call(101, -100..100)) end end end color-2.2.0/test/fixtures/0000755000004100000410000000000015137215736015523 5ustar www-datawww-datacolor-2.2.0/test/fixtures/cielab.json0000644000004100000410000001525615137215736017646 0ustar www-datawww-data[ { "c1": { "L": "50.0000", "a": "2.6772", "b": "-79.7751" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "-82.7485" }, "∂E2000": "2.0425" }, { "c1": { "L": "50.0000", "a": "3.1571", "b": "-77.2803" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "-82.7485" }, "∂E2000": "2.8615" }, { "c1": { "L": "50.0000", "a": "2.8361", "b": "-74.0200" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "-82.7485" }, "∂E2000": "3.4412" }, { "c1": { "L": "50.0000", "a": "-1.3802", "b": "-84.2814" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "-82.7485" }, "∂E2000": "1.0000" }, { "c1": { "L": "50.0000", "a": "-1.1848", "b": "-84.8006" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "-82.7485" }, "∂E2000": "1.0000" }, { "c1": { "L": "50.0000", "a": "-0.9009", "b": "-85.5211" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "-82.7485" }, "∂E2000": "1.0000" }, { "c1": { "L": "50.0000", "a": "0.0000", "b": "0.0000" }, "c2": { "L": "50.0000", "a": "-1.0000", "b": "2.0000" }, "∂E2000": "2.3669" }, { "c1": { "L": "50.0000", "a": "-1.0000", "b": "2.0000" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "0.0000" }, "∂E2000": "2.3669" }, { "c1": { "L": "50.0000", "a": "2.4900", "b": "-0.0010" }, "c2": { "L": "50.0000", "a": "-2.4900", "b": "0.0009" }, "∂E2000": "7.1792" }, { "c1": { "L": "50.0000", "a": "2.4900", "b": "-0.0010" }, "c2": { "L": "50.0000", "a": "-2.4900", "b": "0.0010" }, "∂E2000": "7.1792" }, { "c1": { "L": "50.0000", "a": "2.4900", "b": "-0.0010" }, "c2": { "L": "50.0000", "a": "-2.4900", "b": "0.0011" }, "∂E2000": "7.2195" }, { "c1": { "L": "50.0000", "a": "2.4900", "b": "-0.0010" }, "c2": { "L": "50.0000", "a": "-2.4900", "b": "0.0012" }, "∂E2000": "7.2195" }, { "c1": { "L": "50.0000", "a": "-0.0010", "b": "2.4900" }, "c2": { "L": "50.0000", "a": "0.0009", "b": "-2.4900" }, "∂E2000": "4.8045" }, { "c1": { "L": "50.0000", "a": "-0.0010", "b": "2.4900" }, "c2": { "L": "50.0000", "a": "0.0010", "b": "-2.4900" }, "∂E2000": "4.8045" }, { "c1": { "L": "50.0000", "a": "-0.0010", "b": "2.4900" }, "c2": { "L": "50.0000", "a": "0.0011", "b": "-2.4900" }, "∂E2000": "4.7461" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "50.0000", "a": "0.0000", "b": "-2.5000" }, "∂E2000": "4.3065" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "73.0000", "a": "25.0000", "b": "-18.0000" }, "∂E2000": "27.1492" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "61.0000", "a": "-5.0000", "b": "29.0000" }, "∂E2000": "22.8977" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "56.0000", "a": "-27.0000", "b": "-3.0000" }, "∂E2000": "31.9030" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "58.0000", "a": "24.0000", "b": "15.0000" }, "∂E2000": "19.4535" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "50.0000", "a": "3.1736", "b": "0.5854" }, "∂E2000": "1.0000" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "50.0000", "a": "3.2972", "b": "0.0000" }, "∂E2000": "1.0000" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "50.0000", "a": "1.8634", "b": "0.5757" }, "∂E2000": "1.0000" }, { "c1": { "L": "50.0000", "a": "2.5000", "b": "0.0000" }, "c2": { "L": "50.0000", "a": "3.2592", "b": "0.3350" }, "∂E2000": "1.0000" }, { "c1": { "L": "60.2574", "a": "-34.0099", "b": "36.2677" }, "c2": { "L": "60.4626", "a": "-34.1751", "b": "39.4387" }, "∂E2000": "1.2644" }, { "c1": { "L": "63.0109", "a": "-31.0961", "b": "-5.8663" }, "c2": { "L": "62.8187", "a": "-29.7946", "b": "-4.0864" }, "∂E2000": "1.2630" }, { "c1": { "L": "61.2901", "a": "3.7196", "b": "-5.3901" }, "c2": { "L": "61.4292", "a": "2.2480", "b": "-4.9620" }, "∂E2000": "1.8731" }, { "c1": { "L": "35.0831", "a": "-44.1164", "b": "3.7933" }, "c2": { "L": "35.0232", "a": "-40.0716", "b": "1.5901" }, "∂E2000": "1.8645" }, { "c1": { "L": "22.7233", "a": "20.0904", "b": "-46.6940" }, "c2": { "L": "23.0331", "a": "14.9730", "b": "-42.5619" }, "∂E2000": "2.0373" }, { "c1": { "L": "36.4612", "a": "47.8580", "b": "18.3852" }, "c2": { "L": "36.2715", "a": "50.5065", "b": "21.2231" }, "∂E2000": "1.4146" }, { "c1": { "L": "90.8027", "a": "-2.0831", "b": "1.4410" }, "c2": { "L": "91.1528", "a": "-1.6435", "b": "0.0447" }, "∂E2000": "1.4441" }, { "c1": { "L": "90.9257", "a": "-0.5406", "b": "-0.9208" }, "c2": { "L": "88.6381", "a": "-0.8985", "b": "-0.7239" }, "∂E2000": "1.5381" }, { "c1": { "L": "6.7747", "a": "-0.2908", "b": "-2.4247" }, "c2": { "L": "5.8714", "a": "-0.0985", "b": "-2.2286" }, "∂E2000": "0.6377" }, { "c1": { "L": "2.0776", "a": "0.0795", "b": "-1.1350" }, "c2": { "L": "0.9033", "a": "-0.0636", "b": "-0.5514" }, "∂E2000": "0.9082" } ] color-2.2.0/test/test_yiq.rb0000644000004100000410000000410615137215736016041 0ustar www-datawww-data# frozen_string_literal: true require "color" require "minitest_helper" module TestColor class TestYIQ < Minitest::Test def setup @yiq = Color::YIQ.from_fraction(0.1, 0.2, 0.3) end def test_brightness assert_in_tolerance(0.1, @yiq.brightness) end def test_i assert_in_tolerance(0.2, @yiq.i) assert_in_tolerance(0.2, @yiq.i) end def test_q assert_in_tolerance(0.3, @yiq.q) assert_in_tolerance(0.3, @yiq.q) end def test_y assert_in_tolerance(0.1, @yiq.y) assert_in_tolerance(0.1, @yiq.y) end def test_inspect assert_equal("YIQ [10.00% 20.00% 30.00%]", @yiq.inspect) end def test_pretty_print assert_pretty_inspect "YIQ\n[10.00%\n 20.00%\n 30.00%]\n", @yiq end end class TestYIQConversions < Minitest::Test def setup @yiq = Color::YIQ.from_fraction(0.1, 0.2, 0.3) end def test_to_cmyk skip("to_cmyk conversion not yet implemented") cmyk = @yiq.to_cmyk assert_kind_of(Color::CMYK, cmyk) end def test_to_grayscale gs = @yiq.to_grayscale assert_kind_of(Color::Grayscale, gs) assert_in_tolerance(0.1, gs.g) assert_equal(Color::Grayscale.from_fraction(0), Color::YIQ.from_values(0, 0, 0).to_grayscale) end def test_to_hsl skip("to_hsl conversion not yet implemented") hsl = @yiq.to_hsl assert_kind_of(Color::HSL, hsl) end def test_to_lab skip("to_lab conversion not yet implemented") lab = @yiq.to_lab assert_kind_of(Color::CIELAB, lab) end def test_to_rgb skip("to_rgb conversion not yet implemented") rgb = @yiq.to_rgb assert_kind_of(Color::RGB, rgb) end def test_to_xyz skip("to_xyz conversion not yet implemented") xyz = @yiq.to_xyz assert_kind_of(Color::XYZ, xyz) end def test_to_yiq assert_equal(@yiq, @yiq.to_yiq) assert_kind_of(Color::YIQ, @yiq.to_yiq) end def test_to_internal assert_equal([0.0, 0.0, 0.0], Color::YIQ.from_values(0, 0, 0).to_internal) end end end color-2.2.0/test/test_rgb.rb0000644000004100000410000004240415137215736016014 0ustar www-datawww-data# frozen_string_literal: true require "color" require "json" require "minitest_helper" module TestColor class TestRGB < Minitest::Test def test_adjust_brightness assert_equal("#1a1aff", Color::RGB::Blue.adjust_brightness(10).html) assert_equal("#0000e6", Color::RGB::Blue.adjust_brightness(-10).html) end def test_adjust_hue assert_equal("#6600ff", Color::RGB::Blue.adjust_hue(10).html) assert_equal("#0066ff", Color::RGB::Blue.adjust_hue(-10).html) end def test_adjust_saturation assert_equal("#ef9374", Color::RGB::DarkSalmon.adjust_saturation(10).html) assert_equal("#e39980", Color::RGB::DarkSalmon.adjust_saturation(-10).html) end def test_red red = Color::RGB::Red assert_in_tolerance(1.0, red.r) assert_in_tolerance(100, red.red_p) assert_in_tolerance(255, red.red) assert_in_tolerance(1.0, red.r) end def test_green lime = Color::RGB::Lime assert_in_tolerance(1.0, lime.g) assert_in_tolerance(100, lime.green_p) assert_in_tolerance(255, lime.green) end def test_blue blue = Color::RGB::Blue assert_in_tolerance(1.0, blue.b) assert_in_tolerance(255, blue.blue) assert_in_tolerance(100, blue.blue_p) end def test_brightness assert_in_tolerance(0.0, Color::RGB::Black.brightness) assert_in_tolerance(0.5, Color::RGB::Grey50.brightness) assert_in_tolerance(1.0, Color::RGB::White.brightness) end def test_darken_by assert_in_tolerance(0.5, Color::RGB::Blue.darken_by(50).b) end def test_html assert_equal("#000000", Color::RGB::Black.html) assert_equal(Color::RGB::Black, Color::RGB.from_html("#000000")) assert_equal("#0000ff", Color::RGB::Blue.html) assert_equal("#00ff00", Color::RGB::Lime.html) assert_equal("#ff0000", Color::RGB::Red.html) assert_equal("#ffffff", Color::RGB::White.html) end def test_css assert_equal("rgb(0 0 0)", Color::RGB::Black.css) assert_equal("rgb(0 0 100.00%)", Color::RGB::Blue.css) assert_equal("rgb(0 100.00% 0)", Color::RGB::Lime.css) assert_equal("rgb(100.00% 0 0)", Color::RGB::Red.css) assert_equal("rgb(100.00% 100.00% 100.00%)", Color::RGB::White.css) assert_equal("rgb(0 0 0 / 1.00)", Color::RGB::Black.css(alpha: 1)) assert_equal("rgb(0 0 100.00% / 1.00)", Color::RGB::Blue.css(alpha: 1)) assert_equal("rgb(0 100.00% 0 / 1.00)", Color::RGB::Lime.css(alpha: 1)) assert_equal("rgb(100.00% 0 0 / 1.00)", Color::RGB::Red.css(alpha: 1)) assert_equal("rgb(100.00% 100.00% 100.00% / 1.00)", Color::RGB::White.css(alpha: 1)) assert_equal("rgb(100.00% 0 0 / 0.50)", Color::RGB::Red.css(alpha: 0.5)) end def test_lighten_by assert_in_tolerance(1.0, Color::RGB::Blue.lighten_by(50).b) assert_in_tolerance(0.5, Color::RGB::Blue.lighten_by(50).r) assert_in_tolerance(0.5, Color::RGB::Blue.lighten_by(50).g) end def test_mix_with assert_in_tolerance(0.5, Color::RGB::Red.mix_with(Color::RGB::Blue, 50).r) assert_in_tolerance(0.0, Color::RGB::Red.mix_with(Color::RGB::Blue, 50).g) assert_in_tolerance(0.5, Color::RGB::Red.mix_with(Color::RGB::Blue, 50).b) assert_in_tolerance(0.5, Color::RGB::Blue.mix_with(Color::RGB::Red, 50).r) assert_in_tolerance(0.0, Color::RGB::Blue.mix_with(Color::RGB::Red, 50).g) assert_in_tolerance(0.5, Color::RGB::Blue.mix_with(Color::RGB::Red, 50).b) end def test_closest_match # It should match Blue to Indigo (very simple match) match_from = [Color::RGB::Red, Color::RGB::Green, Color::RGB::Blue] assert_equal(Color::RGB::Blue, Color::RGB::Indigo.closest_match(match_from)) # But fails if using the :just_noticeable difference. assert_nil(Color::RGB::Indigo.closest_match(match_from, threshold_distance: :just_noticeable)) # Crimson & Firebrick are visually closer than DarkRed and Firebrick # (more precise match) match_from += [Color::RGB::DarkRed, Color::RGB::Crimson] assert_equal(Color::RGB::Crimson, Color::RGB::Firebrick.closest_match(match_from)) # Specifying a threshold low enough will cause even that match to fail, though. assert_nil(Color::RGB::Firebrick.closest_match(match_from, threshold_distance: 8.0)) # If the match_from list is an empty array, it also returns nil assert_nil(Color::RGB::Red.closest_match([])) # RGB::Green is 0,128,0, so we'll pick something VERY close to it, visually jnd_green = Color::RGB.from_values(3, 132, 3) assert_equal(Color::RGB::Green, jnd_green.closest_match(match_from, threshold_distance: :jnd)) # And then something that's just barely out of the tolerance range diff_green = Color::RGB.from_values(9, 142, 9) assert_nil(diff_green.closest_match(match_from, threshold_distance: :jnd)) end def test_mean_grayscale c1 = Color::RGB.from_values(0x85, 0x80, 0x00) c1.max_rgb_as_grayscale c1_max = c1.max_rgb_as_grayscale c1_result = Color::Grayscale.from_fraction(0x85 / 255.0) assert_equal(c1_result, c1_max) end def test_from_html assert_equal("RGB [#333333]", Color::RGB.from_html("#333").inspect) assert_equal("RGB [#333333]", Color::RGB.from_html("333").inspect) assert_equal("RGB [#555555]", Color::RGB.from_html("#555555").inspect) assert_equal("RGB [#555555]", Color::RGB.from_html("555555").inspect) assert_raises(ArgumentError) { Color::RGB.from_html("#5555555") } assert_raises(ArgumentError) { Color::RGB.from_html("5555555") } assert_raises(ArgumentError) { Color::RGB.from_html("#55555") } assert_raises(ArgumentError) { Color::RGB.from_html("55555") } end def test_by_hex assert_same(Color::RGB::Cyan, Color::RGB.by_hex("#0ff")) assert_same(Color::RGB::Cyan, Color::RGB.by_hex("#00ffff")) assert_equal("RGB [#333333] {tungsten}", Color::RGB.by_hex("#333").inspect) assert_equal("RGB [#333333] {tungsten}", Color::RGB.by_hex("333").inspect) assert_raises(ArgumentError) { Color::RGB.by_hex("5555555") } assert_raises(ArgumentError) { Color::RGB.by_hex("#55555") } end def test_by_name assert_same(Color::RGB::Cyan, Color::RGB.by_name("cyan")) fetch_error = if RUBY_VERSION < "1.9" IndexError else KeyError end assert_raises(fetch_error) { Color::RGB.by_name("cyanide") } assert_equal(:boom, Color::RGB.by_name("cyanide") { :boom }) end def test_by_css assert_same(Color::RGB::Cyan, Color::RGB.by_css("#0ff")) assert_same(Color::RGB::Cyan, Color::RGB.by_css("cyan")) assert_raises(ArgumentError) { Color::RGB.by_css("cyanide") } end def test_extract_colors assert_equal([Color::RGB::BlanchedAlmond, Color::RGB::Cyan], Color::RGB.extract_colors("BlanchedAlmond is a nice shade, but #00ffff is not.")) end def test_inspect assert_equal("RGB [#000000] {black}", Color::RGB::Black.inspect) assert_equal("RGB [#0000ff] {blue}", Color::RGB::Blue.inspect) assert_equal("RGB [#00ff00] {lime}", Color::RGB::Lime.inspect) assert_equal("RGB [#ff0000] {red}", Color::RGB::Red.inspect) assert_equal("RGB [#ffffff] {white}", Color::RGB::White.inspect) assert_equal("RGB [#708090] {slategray slategrey}", Color::RGB::SlateGrey.inspect) end def test_pretty_print assert_pretty_inspect("RGB\n[#000000]\n{black}\n", Color::RGB::Black) assert_pretty_inspect("RGB\n[#0000ff]\n{blue}\n", Color::RGB::Blue) assert_pretty_inspect("RGB\n[#00ff00]\n{lime}\n", Color::RGB::Lime) assert_pretty_inspect("RGB\n[#ff0000]\n{red}\n", Color::RGB::Red) assert_pretty_inspect("RGB\n[#ffffff]\n{white}\n", Color::RGB::White) assert_pretty_inspect("RGB\n[#708090]\n{slategray\n slategrey}\n", Color::RGB::SlateGrey) end def test_name assert_equal("black", Color::RGB::Black.name) assert_equal("blue", Color::RGB::Blue.name) assert_equal("lime", Color::RGB::Lime.name) assert_equal("red", Color::RGB::Red.name) assert_equal("white", Color::RGB::White.name) assert_equal("black", Color::RGB.from_values(0, 0, 0).name) assert_equal("blue", Color::RGB.from_values(0, 0, 0xff).name) assert_equal("lime", Color::RGB.from_values(0, 0xff, 0).name) assert_equal("red", Color::RGB.from_values(0xff, 0, 0).name) assert_equal("white", Color::RGB.from_values(0xff, 0xff, 0xff).name) assert_equal("000", Color::RGB.from_values(0, 0, 0, ["000"]).name) assert_equal("00f", Color::RGB.from_values(0, 0, 0xff, ["00f"]).name) assert_equal("0f0", Color::RGB.from_values(0, 0xff, 0, ["0f0"]).name) assert_equal("f00", Color::RGB.from_values(0xff, 0, 0, ["f00"]).name) assert_equal("fff", Color::RGB.from_values(0xff, 0xff, 0xff, ["fff"]).name) end def test_delta_e2000 # test data: # http://www.ece.rochester.edu/~gsharma/ciede2000/ # http://www.ece.rochester.edu/~gsharma/ciede2000/dataNprograms/CIEDE2000.xls # this will also test to_degrees and to_radians by association test_data = JSON.parse(File.read("test/fixtures/cielab.json")).map.with_index { |e, i| { i: i, c1: Color::CIELAB.from_values(e["c1"]["L"].to_f, e["c1"]["a"].to_f, e["c1"]["b"].to_f), c2: Color::CIELAB.from_values(e["c2"]["L"].to_f, e["c2"]["a"].to_f, e["c2"]["b"].to_f), correct_delta: e["∂E2000"].to_f } } test_data.each do |e| e => c1:, c2:, correct_delta: e2000_c1_c2 = c1.delta_e2000(c2) e2000_c2_c1 = c2.delta_e2000(c1) assert_in_tolerance e2000_c1_c2, e2000_c2_c1 assert_in_tolerance e2000_c1_c2, correct_delta end end def test_contrast data = [ [[171, 215, 103], [195, 108, 197], 0.18117], [[223, 133, 234], [64, 160, 101], 0.229530], [[7, 30, 49], [37, 225, 31], 0.377786], [[65, 119, 130], [70, 63, 212], 0.10323], [[211, 77, 232], [5, 113, 139], 0.233503], [[166, 185, 41], [87, 193, 137], 0.07275], [[1, 120, 37], [195, 70, 33], 0.1474640], [[99, 206, 21], [228, 204, 155], 0.22611], [[15, 18, 41], [90, 202, 208], 0.552434] ] data.each do |(c1, c2, value)| c1 = Color::RGB.from_values(c1[0], c1[1], c1[2]) c2 = Color::RGB.from_values(c2[0], c2[1], c2[2]) assert_in_delta(0.0001, c1.contrast(c2), value) assert_equal(c1.contrast(c2), c2.contrast(c1)) end end def test_fix_45_invalid_rgb_to_lab assert_equal( Color::CIELAB[56.2562, 88.1033, -18.8203], Color::RGB.by_hex("#ff00aa").to_lab ) assert_equal("#ff00aa", Color::RGB.by_hex("#ff00aa").to_lab.to_rgb.html) end end class TestRGBConversions < Minitest::Test def test_to_cmyk assert_kind_of(Color::CMYK, Color::RGB::Black.to_cmyk) assert_equal(Color::CMYK.from_percentage(0, 0, 0, 100), Color::RGB::Black.to_cmyk) assert_equal(Color::CMYK.from_percentage(0, 0, 100, 0), Color::RGB::Yellow.to_cmyk) assert_equal(Color::CMYK.from_percentage(100, 0, 0, 0), Color::RGB::Cyan.to_cmyk) assert_equal(Color::CMYK.from_percentage(0, 100, 0, 0), Color::RGB::Magenta.to_cmyk) assert_equal(Color::CMYK.from_percentage(0, 100, 100, 0), Color::RGB::Red.to_cmyk) assert_equal(Color::CMYK.from_percentage(100, 0, 100, 0), Color::RGB::Lime.to_cmyk) assert_equal(Color::CMYK.from_percentage(100, 100, 0, 0), Color::RGB::Blue.to_cmyk) assert_equal(Color::CMYK.from_percentage(10.32, 60.52, 10.32, 39.47), Color::RGB::Purple.to_cmyk) assert_equal(Color::CMYK.from_percentage(10.90, 59.13, 59.13, 24.39), Color::RGB::Brown.to_cmyk) assert_equal(Color::CMYK.from_percentage(0, 63.14, 18.43, 0), Color::RGB::Carnation.to_cmyk) assert_equal(Color::CMYK.from_percentage(7.39, 62.69, 62.69, 37.32), Color::RGB::Cayenne.to_cmyk) end def test_to_grayscale assert_kind_of(Color::Grayscale, Color::RGB::Black.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0), Color::RGB::Black.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0.5), Color::RGB::Yellow.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0.5), Color::RGB::Cyan.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0.5), Color::RGB::Magenta.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0.5), Color::RGB::Red.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0.5), Color::RGB::Lime.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0.5), Color::RGB::Blue.to_grayscale) assert_equal(Color::Grayscale.from_fraction(0.2510), Color::RGB::Purple.to_grayscale) assert_equal(Color::Grayscale.from_percentage(40.58), Color::RGB::Brown.to_grayscale) assert_equal(Color::Grayscale.from_percentage(68.43), Color::RGB::Carnation.to_grayscale) assert_equal(Color::Grayscale.from_percentage(27.65), Color::RGB::Cayenne.to_grayscale) end def test_to_hsl assert_kind_of(Color::HSL, Color::RGB::Black.to_hsl) assert_equal(Color::HSL.from_values(0, 0, 0), Color::RGB::Black.to_hsl) assert_equal(Color::HSL.from_values(60, 100, 50), Color::RGB::Yellow.to_hsl) assert_equal(Color::HSL.from_values(180, 100, 50), Color::RGB::Cyan.to_hsl) assert_equal(Color::HSL.from_values(300, 100, 50), Color::RGB::Magenta.to_hsl) assert_equal(Color::HSL.from_values(0, 100, 50), Color::RGB::Red.to_hsl) assert_equal(Color::HSL.from_values(120, 100, 50), Color::RGB::Lime.to_hsl) assert_equal(Color::HSL.from_values(240, 100, 50), Color::RGB::Blue.to_hsl) assert_equal(Color::HSL.from_values(300, 100, 25.10), Color::RGB::Purple.to_hsl) assert_equal(Color::HSL.from_values(0, 59.42, 40.59), Color::RGB::Brown.to_hsl) assert_equal(Color::HSL.from_values(317.5, 100, 68.43), Color::RGB::Carnation.to_hsl) assert_equal(Color::HSL.from_values(0, 100, 27.64), Color::RGB::Cayenne.to_hsl) # The following tests a bug reported by Jean Krohn on 10 June 2006 where HSL # conversion was not quite correct, resulting in a bad round-trip. assert_equal("RGB [#008800]", Color::RGB.from_html("#008800").to_hsl.to_rgb.inspect) refute_equal("RGB [#002288]", Color::RGB.from_html("#008800").to_hsl.to_rgb.inspect) # The following tests a bug reported by Adam Johnson on 29 October # 2010. hsl = Color::HSL.from_values(262, 67, 42) c = Color::RGB.from_fraction(0.34496, 0.1386, 0.701399).to_hsl assert_in_tolerance hsl.h, c.h, "Hue" assert_in_tolerance hsl.s, c.s, "Saturation" assert_in_tolerance hsl.l, c.l, "Luminance" end def test_to_lab # Luminosity can be an absolute. assert_in_tolerance(0.0, Color::RGB::Black.to_lab.l) assert_in_tolerance(100.0, Color::RGB::White.to_lab.l) # It's not really possible to have absolute # numbers here because of how L*a*b* works, but # negative/positive comparisons work assert(Color::RGB::Green.to_lab.a < 0) assert(Color::RGB::Magenta.to_lab.a > 0) assert(Color::RGB::Blue.to_lab.b < 0) assert(Color::RGB::Yellow.to_lab.b > 0) end # # An RGB color round-tripped through CIELAB should still have more or less the same # # RGB values, but this doesn't really work because the color math here is slightly # # wrong. # def test_to_lab_automated # 10.times do |t| # c1 = Color::RGB.from_values(rand(256), rand(256), rand(256)) # l1 = c1.to_lab # c2 = l1.to_rgb # # assert_in_tolerance(c1.r, c2.r) # assert_in_tolerance(c1.g, c2.g) # assert_in_tolerance(c1.b, c2.b) # end # end def test_to_rgb assert_same(Color::RGB::Black, Color::RGB::Black.to_rgb) end def test_to_xyz assert_kind_of(Color::XYZ, Color::RGB::Black.to_xyz) assert_equal(Color::XYZ.from_values(0, 0, 0), Color::RGB::Black.to_xyz) assert_equal(Color::XYZ.from_fraction(0.4124564, 0.2126729, 0.0193339), Color::RGB::Red.to_xyz) assert_equal(Color::XYZ.from_fraction(0.0771865, 0.1543731, 0.0257288), Color::RGB::Green.to_xyz) assert_equal(Color::XYZ.from_fraction(0.1804375, 0.072175, 0.9503041), Color::RGB::Blue.to_xyz) end def test_to_yiq assert_kind_of(Color::YIQ, Color::RGB::Black.to_yiq) assert_equal(Color::YIQ.from_values(0, 0, 0), Color::RGB::Black.to_yiq) assert_equal(Color::YIQ.from_values(88.6, 32.1, 0), Color::RGB::Yellow.to_yiq) assert_equal(Color::YIQ.from_values(70.1, 0, 0), Color::RGB::Cyan.to_yiq) assert_equal(Color::YIQ.from_values(41.3, 27.5, 52.3), Color::RGB::Magenta.to_yiq) assert_equal(Color::YIQ.from_values(29.9, 59.6, 21.2), Color::RGB::Red.to_yiq) assert_equal(Color::YIQ.from_values(58.7, 0, 0), Color::RGB::Lime.to_yiq) assert_equal(Color::YIQ.from_values(11.4, 0, 31.1), Color::RGB::Blue.to_yiq) assert_equal(Color::YIQ.from_values(20.73, 13.80, 26.25), Color::RGB::Purple.to_yiq) assert_equal(Color::YIQ.from_values(30.89, 28.75, 10.23), Color::RGB::Brown.to_yiq) assert_equal(Color::YIQ.from_values(60.84, 23.28, 27.29), Color::RGB::Carnation.to_yiq) assert_equal(Color::YIQ.from_values(16.53, 32.96, 11.72), Color::RGB::Cayenne.to_yiq) end def test_to_internal assert_equal([0.0, 0.0, 0.0], Color::RGB::Black.to_internal) end end end color-2.2.0/test/test_hsl.rb0000644000004100000410000001226415137215736016031 0ustar www-datawww-data# frozen_string_literal: true require "color" require "minitest_helper" module TestColor class TestHSL < Minitest::Test def setup @hsl = Color::HSL.from_values(145, 20, 30) end def test_rgb_roundtrip_conversion hsl = Color::HSL.from_values(262, 67, 42) c = hsl.to_rgb.to_hsl assert_in_tolerance hsl.h, c.h, "Hue" assert_in_tolerance hsl.s, c.s, "Saturation" assert_in_tolerance hsl.l, c.l, "Luminance" end def test_brightness assert_in_tolerance 0.3, @hsl.brightness end def test_hue assert_in_tolerance 0.4027, @hsl.h assert_in_tolerance 145, @hsl.hue # @hsl.hue = 33 # assert_in_tolerance 0.09167, @hsl.h # @hsl.hue = -33 # assert_in_tolerance 0.90833, @hsl.h # @hsl.h = 3.3 # assert_in_tolerance 360, @hsl.hue # @hsl.h = -3.3 # assert_in_tolerance 0.0, @hsl.h # @hsl.hue = 0 # @hsl.hue -= 20 # assert_in_tolerance 340, @hsl.hue # @hsl.hue += 45 # assert_in_tolerance 25, @hsl.hue end def test_saturation assert_in_tolerance 0.2, @hsl.s assert_in_tolerance 20, @hsl.saturation # @hsl.saturation = 33 # assert_in_tolerance 0.33, @hsl.s # @hsl.s = 3.3 # assert_in_tolerance 100, @hsl.saturation # @hsl.s = -3.3 # assert_in_tolerance 0.0, @hsl.s end def test_luminance assert_in_tolerance 0.3, @hsl.l assert_in_tolerance 30, @hsl.luminosity # @hsl.luminosity = 33 # assert_in_tolerance 0.33, @hsl.l # @hsl.l = 3.3 # assert_in_tolerance 100, @hsl.lightness # @hsl.l = -3.3 # assert_in_tolerance 0.0, @hsl.l end def test_css assert_equal "hsl(145.00deg 20.00% 30.00%)", @hsl.css assert_equal "hsl(145.00deg 20.00% 30.00% / 1.00)", @hsl.css(alpha: 1) end def test_mix_with red = Color::RGB::Red.to_hsl yellow = Color::RGB::Yellow.to_hsl assert_in_tolerance 0, red.hue assert_in_tolerance 60, yellow.hue ry25 = red.mix_with yellow, 0.25 assert_in_tolerance 15, ry25.hue ry50 = red.mix_with yellow, 0.50 assert_in_tolerance 30, ry50.hue ry75 = red.mix_with yellow, 0.75 assert_in_tolerance 45, ry75.hue end def test_inspect assert_equal "HSL [145.00deg 20.00% 30.00%]", @hsl.inspect end def test_pretty_print assert_pretty_inspect "HSL\n[145.00deg\n 20.00%\n 30.00%]\n", @hsl end end class TestHSLConversions < Minitest::Test def setup @hsl = Color::HSL.from_values(145, 20, 30) end def test_to_cmyk cmyk = @hsl.to_cmyk assert_kind_of(Color::CMYK, cmyk) assert_in_tolerance(0.3223, cmyk.c) assert_in_tolerance(0.2023, cmyk.m) assert_in_tolerance(0.2723, cmyk.y) assert_in_tolerance(0.4377, cmyk.k) assert_equal(Color::CMYK.from_percentage(0, 0, 0, 100), Color::HSL.from_values(0, 0, 0).to_cmyk) end def test_to_grayscale gs = @hsl.to_grayscale assert_kind_of(Color::Grayscale, gs) assert_in_tolerance(30, gs.gray) assert_equal(Color::Grayscale.from_fraction(0), Color::HSL.from_values(0, 0, 0).to_grayscale) end def test_to_hsl assert_equal(@hsl, @hsl.to_hsl) assert_kind_of(Color::HSL, @hsl.to_hsl) end def test_to_lab lab = @hsl.to_lab assert_kind_of(Color::CIELAB, lab) assert_in_tolerance(36.19817, lab.l) assert_in_tolerance(-15.6, lab.a) assert_in_tolerance(6.72317, lab.b) assert_equal(Color::CIELAB.from_values(0, 0, 0), Color::HSL.from_values(0, 0, 0).to_lab) end def test_to_rgb rgb = @hsl.to_rgb assert_kind_of(Color::RGB, rgb) assert_in_tolerance(0.24, rgb.r) assert_in_tolerance(0.36, rgb.g) assert_in_tolerance(0.29, rgb.b) # The following tests address a bug reported by Jean Krohn on June 6, # 2006 and exercise some previously unexercised code in to_rgb. assert_equal(Color::RGB::Black, Color::HSL.from_values(75, 75, 0)) assert_equal(Color::RGB::White, Color::HSL.from_values(75, 75, 100)) assert_equal(Color::RGB::Gray80, Color::HSL.from_values(75, 0, 80)) # The following tests a bug reported by Adam Johnson on 29 October # 2010. rgb = Color::RGB.from_fraction(0.34496, 0.1386, 0.701399) c = Color::HSL.from_values(262, 67, 42).to_rgb assert_in_tolerance(rgb.r, c.r, "Red") assert_in_tolerance(rgb.g, c.g, "Green") assert_in_tolerance(rgb.b, c.b, "Blue") end def test_to_xyz xyz = @hsl.to_xyz assert_kind_of(Color::XYZ, xyz) assert_in_tolerance(0.069805, xyz.x) assert_in_tolerance(0.091115, xyz.y) assert_in_tolerance(0.078593, xyz.z) assert_equal(Color::XYZ.from_values(0, 0, 0), Color::HSL.from_values(0, 0, 0).to_xyz) end def test_to_yiq yiq = @hsl.to_yiq assert_kind_of(Color::YIQ, yiq) assert_in_tolerance(0.3161, yiq.y) assert_in_tolerance(0.0, yiq.i) assert_in_tolerance(0.0, yiq.q) assert_equal(Color::YIQ.from_values(0, 0, 0), Color::HSL.from_values(0, 0, 0).to_yiq) end def test_to_internal assert_equal([0.0, 0.0, 0.0], Color::HSL.from_values(0, 0, 0).to_internal) end end end color-2.2.0/test/test_grayscale.rb0000644000004100000410000000662715137215736017223 0ustar www-datawww-data# frozen_string_literal: true require "color" require "minitest_helper" module TestColor class TestGrayscale < Minitest::Test def setup @gs = Color::Grayscale.from_percentage(33) end def test_brightness assert_in_tolerance(0.33, @gs.brightness) end def test_darken_by assert_in_tolerance(29.7, @gs.darken_by(10).gray) end def test_g assert_in_tolerance(0.33, @gs.g) assert_in_tolerance(33, @gs.gray) # @gs.gray = 40 # assert_in_tolerance(0.4, @gs.g) # @gs.g = 2.0 # assert_in_tolerance(100, @gs.gray) # @gs.gray = -2.0 # assert_in_tolerance(0.0, @gs.g) end def test_css assert_equal("#545454", @gs.html) assert_equal("rgb(33.00% 33.00% 33.00%)", @gs.css) assert_equal("rgb(33.00% 33.00% 33.00% / 1.00)", @gs.css(alpha: 1)) assert_equal("rgb(33.00% 33.00% 33.00% / 0.20)", @gs.css(alpha: 0.2)) end def test_lighten_by assert_in_tolerance(0.363, @gs.lighten_by(10).g) end def test_inspect assert_equal("Grayscale [33.00%]", @gs.inspect) end def test_pretty_print assert_pretty_inspect "Grayscale\n[33.00%]\n", @gs end end class TestGrayscaleConversions < Minitest::Test def setup @gs = Color::Grayscale.from_percentage(33) end def test_to_cmyk cmyk = @gs.to_cmyk assert_kind_of(Color::CMYK, cmyk) assert_in_tolerance(0.0, cmyk.c) assert_in_tolerance(0.0, cmyk.m) assert_in_tolerance(0.0, cmyk.y) assert_in_tolerance(0.67, cmyk.k) assert_equal(Color::CMYK.from_percentage(0, 0, 0, 100), Color::Grayscale.from_percentage(0).to_cmyk) end def test_to_grayscale assert_equal(@gs, @gs.to_grayscale) assert_kind_of(Color::Grayscale, @gs.to_grayscale) end def test_to_hsl hsl = @gs.to_hsl assert_kind_of(Color::HSL, hsl) assert_in_tolerance(0.0, hsl.h) assert_in_tolerance(0.0, hsl.s) assert_in_tolerance(0.33, hsl.l) assert_equal(Color::HSL.from_values(0, 0, 0), Color::Grayscale.from_percentage(0).to_hsl) end def test_to_lab lab = @gs.to_lab assert_kind_of(Color::CIELAB, lab) assert_in_tolerance(35.78746, lab.l) assert_in_tolerance(0.0031199, lab.a) assert_in_tolerance(-0.000639, lab.b) assert_equal(Color::CIELAB.from_values(0, 0, 0), Color::Grayscale.from_percentage(0).to_lab) end def test_to_rgb rgb = @gs.to_rgb assert_kind_of(Color::RGB, rgb) assert_in_tolerance(0.33, rgb.r) assert_in_tolerance(0.33, rgb.g) assert_in_tolerance(0.33, rgb.b) assert_equal(Color::RGB.from_values(0, 0, 0), Color::Grayscale.from_percentage(0).to_rgb) end def test_to_xyz xyz = @gs.to_xyz assert_kind_of(Color::XYZ, xyz) assert_in_tolerance(0.08457, xyz.x) assert_in_tolerance(0.08898, xyz.y) assert_in_tolerance(0.09688, xyz.z) assert_equal(Color::XYZ.from_values(0, 0, 0), Color::Grayscale.from_percentage(0).to_xyz) end def test_to_yiq yiq = @gs.to_yiq assert_kind_of(Color::YIQ, yiq) assert_in_tolerance(0.33, yiq.y) assert_in_tolerance(0.0, yiq.i) assert_in_tolerance(0.0, yiq.q) assert_equal(Color::YIQ.from_values(0, 0, 0), Color::Grayscale.from_percentage(0).to_yiq) end def test_to_internal assert_equal([0.0], Color::Grayscale.from_percentage(0).to_internal) end end end color-2.2.0/test/test_cmyk.rb0000644000004100000410000001027515137215736016206 0ustar www-datawww-data# frozen_string_literal: true require "color" require "minitest_helper" module TestColor class TestCMYK < Minitest::Test def setup @cmyk = Color::CMYK.from_percentage(10, 20, 30, 40) end def test_cyan assert_in_tolerance(0.1, @cmyk.c) assert_in_tolerance(10, @cmyk.cyan) assert_in_tolerance(0.0, Color::CMYK[-1.0, 20, 30, 40].cyan) end def test_magenta assert_in_tolerance(0.2, @cmyk.m) assert_in_tolerance(20, @cmyk.magenta) end def test_yellow assert_in_tolerance(0.3, @cmyk.y) assert_in_tolerance(30, @cmyk.yellow) end def test_black assert_in_tolerance(0.4, @cmyk.k) assert_in_tolerance(40, @cmyk.black) end def test_inspect assert_equal("CMYK [10.00% 20.00% 30.00% 40.00%]", @cmyk.inspect) end def test_pretty_print assert_pretty_inspect "CMYK\n[10.00%\n 20.00%\n 30.00%\n 40.00%]\n", @cmyk end def test_css assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00%, rgb(54.00% 48.00% 42.00%))", @cmyk.css) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00% / 0.50, rgb(54.00% 48.00% 42.00% / 0.50))", @cmyk.css(alpha: 0.5)) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00%)", @cmyk.css(fallback: false)) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00% / 0.50)", @cmyk.css(alpha: 0.5, fallback: false)) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00%, rgb(0 0 100.00%))", @cmyk.css(fallback: Color::RGB::Blue)) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00% / 0.50, rgb(0 0 100.00% / 0.50))", @cmyk.css(alpha: 0.5, fallback: Color::RGB::Blue)) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00% / 0.50, rgb(0 0 100.00%))", @cmyk.css(alpha: 0.5, fallback: {color: Color::RGB::Blue})) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00% / 0.50, rgb(0 0 100.00% / 0.30))", @cmyk.css(alpha: 0.5, fallback: {color: Color::RGB::Blue, alpha: 0.3})) assert_equal("device-cmyk(10.00% 20.00% 30.00% 40.00% / 0.50, rgb(54.00% 48.00% 42.00% / 0.30))", @cmyk.css(alpha: 0.5, fallback: {alpha: 0.3})) end end class TestCMYKConversions < Minitest::Test def setup @cmyk = Color::CMYK.from_percentage(10, 20, 30, 40) end def test_to_cmyk assert_equal(@cmyk, @cmyk.to_cmyk) assert_kind_of(Color::CMYK, @cmyk.to_cmyk) end def test_to_grayscale gs = @cmyk.to_grayscale assert_kind_of(Color::Grayscale, gs) assert_in_tolerance(0.4185, gs.g) end def test_to_hsl hsl = @cmyk.to_hsl assert_kind_of(Color::HSL, hsl) assert_in_tolerance(0.48, hsl.l) assert_in_tolerance(0.125, hsl.s) assert_in_tolerance(0.08333, hsl.h) end def test_to_lab lab = @cmyk.to_lab assert_kind_of(Color::CIELAB, lab) assert_in_tolerance(52.35269, lab.l) assert_in_tolerance(3.268274, lab.a) assert_in_tolerance(10.52531, lab.b) assert_equal(Color::CIELAB.from_values(0, 0, 0), Color::CMYK.from_percentage(0, 0, 0, 100).to_lab) end def test_to_rgb rgb_adobe = @cmyk.to_rgb(rgb_method: :adobe) assert_kind_of(Color::RGB, rgb_adobe) assert_in_tolerance(0.5, rgb_adobe.r) assert_in_tolerance(0.4, rgb_adobe.g) assert_in_tolerance(0.3, rgb_adobe.b) rgb = @cmyk.to_rgb assert_kind_of(Color::RGB, rgb) assert_in_tolerance(0.54, rgb.r) assert_in_tolerance(0.48, rgb.g) assert_in_tolerance(0.42, rgb.b) end def test_to_xyz xyz = @cmyk.to_xyz assert_kind_of(Color::XYZ, xyz) assert_in_tolerance(0.200995, xyz.x) assert_in_tolerance(0.204594, xyz.y) assert_in_tolerance(0.168249, xyz.z) assert_equal(Color::XYZ.from_values(0, 0, 0), Color::CMYK.from_percentage(0, 0, 0, 100).to_xyz) end def test_to_yiq yiq = @cmyk.to_yiq assert_kind_of(Color::YIQ, yiq) assert_in_tolerance(0.4911, yiq.y) assert_in_tolerance(0.05502, yiq.i) assert_in_tolerance(0.0, yiq.q) assert_equal(Color::YIQ.from_values(0, 0, 0), Color::CMYK.from_percentage(0, 0, 0, 100).to_yiq) end def test_to_internal assert_equal([0.0, 0.0, 0.0, 1.0], Color::CMYK.from_percentage(0, 0, 0, 100).to_internal) end end end color-2.2.0/Rakefile0000644000004100000410000000420315137215736014337 0ustar www-datawww-data# frozen_string_literal: true require "rubygems" require "hoe" require "rake/clean" require "rdoc/task" require "minitest" require "minitest/test_task" Hoe.plugin :halostatue Hoe.plugins.delete :debug Hoe.plugins.delete :newb Hoe.plugins.delete :publish Hoe.plugins.delete :signing Hoe.plugins.delete :test hoe = Hoe.spec "color" do developer("Austin Ziegler", "halostatue@gmail.com") developer("Matt Lyon", "matt@postsomnia.com") self.trusted_release = ENV["rubygems_release_gem"] == "true" require_ruby_version ">= 3.2" license "MIT" spec_extras[:metadata] = ->(val) { val["rubygems_mfa_required"] = "true" } extra_dev_deps << ["hoe", "~> 4.0"] extra_dev_deps << ["hoe-halostatue", "~> 3.0"] extra_dev_deps << ["json", ">= 0.0"] extra_dev_deps << ["minitest", "~> 6.0"] extra_dev_deps << ["minitest-autotest", "~> 1.0"] extra_dev_deps << ["minitest-focus", "~> 1.1"] extra_dev_deps << ["rake", ">= 10.0", "< 14"] extra_dev_deps << ["rdoc", ">= 6.0", "< 8"] extra_dev_deps << ["simplecov", "~> 0.22"] extra_dev_deps << ["simplecov-lcov", "~> 0.8"] extra_dev_deps << ["standard", "~> 1.50"] end Minitest::TestTask.create :test Minitest::TestTask.create :coverage do |t| formatters = <<-RUBY.split($/).join(" ") SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::LcovFormatter, SimpleCov::Formatter::SimpleFormatter ]) RUBY t.test_prelude = <<-RUBY.split($/).join("; ") require "simplecov" require "simplecov-lcov" SimpleCov::Formatter::LcovFormatter.config do |config| config.report_with_single_file = true config.lcov_file_name = "lcov.info" end SimpleCov.start "test_frameworks" do enable_coverage :branch primary_coverage :branch formatter #{formatters} end RUBY end task default: :test task :version do require "color/version" puts Color::VERSION end RDoc::Task.new do _1.title = "Color -- Color Math with Ruby" _1.main = "lib/color.rb" _1.rdoc_dir = "doc" _1.rdoc_files = hoe.spec.require_paths - ["Manifest.txt"] + hoe.spec.extra_rdoc_files _1.markup = "markdown" end task docs: :rerdoc color-2.2.0/CODE_OF_CONDUCT.md0000644000004100000410000002066215137215736015500 0ustar www-datawww-data# Contributor Covenant 3.0 Code of Conduct ## Our Pledge We pledge to make our community welcoming, safe, and equitable for all. We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. ## Encouraged Behaviors While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: 1. Respecting the **purpose of our community**, our activities, and our ways of gathering. 2. Engaging **kindly and honestly** with others. 3. Respecting **different viewpoints** and experiences. 4. **Taking responsibility** for our actions and contributions. 5. Gracefully giving and accepting **constructive feedback**. 6. Committing to **repairing harm** when it occurs. 7. Behaving in other ways that promote and sustain the **well-being of our community**. ## Restricted Behaviors We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. 5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. 7. Behaving in other ways that **threaten the well-being** of our community. ### Other Restrictions 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. 3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. ## Reporting an Issue Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. When an incident does occur, it is important to report it promptly. To report a possible violation, create a [private security advisory][advisory] — violations of this code of conduct are considered security vulnerabilities. Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. ## Addressing and Repairing Harm If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. 1. Warning 1. Event: A violation involving a single incident or series of incidents. 2. Consequence: A private, written warning from the Community Moderators. 3. Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. 2. Temporarily Limited Activities 1. Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. 2. Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. 3. Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. 3. Temporary Suspension 1. Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. 2. Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. 3. Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. 4. Permanent Ban 1. Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. 2. Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. 3. Repair: There is no possible repair in cases of this severity. This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at . Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit . For answers to common questions about Contributor Covenant, see the FAQ at . Translations are provided at . Additional enforcement and community guideline resources can be found at . The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team][inclusion]. [advisory]: https://github.com/halostatue/color/security/advisories/new [inclusion]: https://github.com/mozilla/inclusion color-2.2.0/CONTRIBUTORS.md0000644000004100000410000000066715137215736015163 0ustar www-datawww-data# Contributors - Austin Ziegler created color-tools. - Matt Lyons created color. - Dave Heitzman (contrast comparison, CIELAB color support) - Thomas Sawyer - Aaron Hill (CIE94 color matching) - Luke Bennellick - [@stiff][gh-user-stiff] (CIELAB color support) - [@r-plus][gh-user-r-plus] - Matthew Draper - Charles Nutter - Paul Gallagher - Alex Chan [gh-user-stiff]: https://github.com/stiff [gh-user-r-plus]: https://github.com/r-plus color-2.2.0/licences/0000755000004100000410000000000015137215736014460 5ustar www-datawww-datacolor-2.2.0/licences/dco.txt0000644000004100000410000000252615137215736015773 0ustar www-datawww-dataDeveloper Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. color-2.2.0/README.md0000644000004100000410000000566015137215736014161 0ustar www-datawww-data# Color -- Color Math in Ruby [![RubyGems Version][shield-gems]][rubygems] ![Coveralls][shield-coveralls] [![Build Status][shield-ci]][ci-workflow] - code :: - issues :: - docs :: - changelog :: ## Description Color is a Ruby library to provide RGB, CMYK, HSL, and other color space manipulation support to applications that require it. It provides optional named RGB colors that are commonly supported in HTML, SVG, and X11 applications. The Color library performs purely mathematical manipulation of the colors based on color theory without reference to device color profiles (such as sRGB or Adobe RGB). For most purposes, when working with RGB and HSL color spaces, this won't matter. Absolute color spaces (like CIE LAB and CIE XYZ) cannot be reliably converted to relative color spaces (like RGB) without color profiles. When necessary for conversions, Color provides D65 and D50 reference white values in Color::XYZ. Color 2.2 adds a minor feature where an RGB color created from values can silently inherit the `#name` of a predefined color if `color/rgb/colors` has already been loaded. It builds on the Color 2.0 major release, dropping support for all versions of Ruby prior to 3.2 as well as removing or renaming a number of features. The main breaking changes are: - Color classes are immutable Data objects; they are no longer mutable. - RGB named colors are no longer loaded on gem startup, but must be required explicitly (this is _not_ done via `autoload` because there are more than 100 named colors with spelling variations) with `require "color/rgb/colors"`. - Color palettes have been removed. - `Color::CSS` and `Color::CSS#[]` have been removed. ## Example Suppose you want to make a given RGB color a little lighter. Adjusting the RGB color curves will change the hue and saturation will also change. Instead, use the CIE LAB color space keeping the `color` components intact, altering only the lightness component: ```ruby c = Color::RGB.from_values(r, g, b).to_lab if (t = c.l / 50.0) < 1 c.l = 50 * ((1.0 - t) * Math.sqrt(t) + t**2) end c.to_rgb ``` ## Color Semantic Versioning The Color library uses a [Semantic Versioning][semver] scheme with one change: - When PATCH is zero (`0`), it will be omitted from version references. [ci-workflow]: https://github.com/halostatue/color/actions/workflows/ci.yml [coveralls]: https://coveralls.io/github/halostatue/color?branch=main [rubygems]: https://rubygems.org/gems/color [semver]: https://semver.org/ [shield-ci]: https://img.shields.io/github/actions/workflow/status/halostatue/color/ci.yml?style=for-the-badge "Build Status" [shield-coveralls]: https://img.shields.io/coverallsCoverage/github/halostatue/color?style=for-the-badge [shield-gems]: https://img.shields.io/gem/v/color?style=for-the-badge "Version" color-2.2.0/CHANGELOG.md0000644000004100000410000004140415137215736014507 0ustar www-datawww-data# color Changelog ## 2.2.0 / 2026-01-DD - When `color/rgb/colors` is loaded and the RGB color does not have defined names, a fallback lookup to named RGB colors will be performed to use that name. ```ruby simple_black = Color.from_values(r: 0, g: 0, b: 0) stuart_black = Color.from_values(r: 0, g: 0, b: 0, names: ["semple-black-4.0"]) simple_black.name # => nil stuart_black.name # => "semple-black-4.0" require 'color/rgb/colors' simple_black.name # => "black" stuart_black.name # => "semple-black-4.0" ``` This was suggested by [@akicho8][gh-user-akicho8] in [#89][gh-issue-89]. - The RGB inspect and pretty print formats have been modified to include defined names. This was suggested by @akicho8 in #89. - Added unit tests for `#pretty_print` implementations. Fixed some bugs found in the implementations. ## 2.1.2 / 2025-12-30 - Updated to Contributor Covenant 3.0 and applied updates to several support documents. - Full coverage of conversion tests. Adds CIELAB `to_yiq` and Grayscale `to_xyz` methods. Only YIQ now lacks conversions support. Fixed in [#69][pull-69]. - Fix an incorrect conversion of CIELAB colors with low lightness to XYZ, which caused the Y component to be ~903 times larger than correct. In practice, this returned unexpectedly bright colours. This also affected conversion from CIELAB to RGB, CMYK, HSL, YIQ, and grayscale, which convert from CIELAB to XYZ as an intermediate step. Reported by [@alexwlchan][gh-user-alexwlchan] in [#95][issue-95] and fixed in [#96][pull-96]. - Fix an incorrect comparison when converting CIE XYZ colors to RGB that could raise a `NoMethodError` when constructing the RGB value. The conversion incorrectly compared the absolute value of an intermediate value against the 0.0031308 threshold instead of comparing the original value, causing certain negative values to follow the wrong branch and return complex RGB components. This also affected conversion from XYZ to CMYK, HSL, and YIK, and from CIELAB to RGB, HSL, YIQ, and Grayscale -- all of which convert from XYZ to RGB as an intermediate step. Reported by @alexwlchan in [#92][issue-92] and fixed in [#93][pull-93]. ## 2.1.1 / 2025-08-08 Color 2.1.1 fixes a bug where `Color::RGB::Black` and `Color::RGB::White` are no longer defined automatically because they are part of `color/rgb/colors`. Internally, this defines `Color::RGB::Black000` and `Color::RGB::WhiteFFF`. ## 2.1.0 / 2025-07-20 Color 2.1.0 fixes a computation bug where CIE XYZ values were improperly clamped and adds more Color::XYZ white points for standard illuminants. - Fixes a bug where standard illuminant white points were improperly clamped and was seen in `Color::RGB#to_lab` since CIELAB conversions must go through the XYZ color model. Even though we were using the D65 white point, the Z value was being clamped to 1.0 instead of the correct value of ≅1.08. Reported by @r-plus in [#45][issue-45] and fixed in [#45][pull-46]. The resulting Color::LAB values are not _exactly_ the same values under Color 1.8, but they are within fractional differences deemed acceptable. - Added more white points for standard illuminants in the Color::XYZ::WP2 constant. The values here were derived from the [White points of standard illuminants][wp-std-illuminant] using the `xyY` to `XYZ` conversion formula where `X = (x * Y) / y` and `Z = ((1 - x - y) * Y) / y`. Only the values for CIE 1931 2° were computed. The values for Color::XYZ::D50 and Color::XYZ::D65 were replaced with these computed values. ## 2.0.1 / 2025-07-05 Color 2.0.1 is a minor documentation update. ## 2.0.0 / 2025-07-05 Color 2.0.0 is a major release of the Color library. ### 💣 Breaking Changes Color 2.0 contains breaking changes. Functionality previously deprecated has been removed, but other functionality has been changed or removed as part of this release without prior warning. - The minimum supported version of Ruby is 3.2. - Color classes are now immutable implementations of Data objects (first introduced in Ruby 3.2). This will restrict Color 2 from running on versions of JRuby before JRuby 10. - The constants `Color::COLOR_VERSION` and `Color::COLOR_TOOLS_VERSION` have been removed; there is only `Color::VERSION`. This reverses a planned deprecation decision made more than ten years ago that no longer makes sense. - All named color classes at `Color` have been removed as planned. - `Color::RGB::BeccaPurple` has been removed as an alias for `Color::RGB::RebeccaPurple`. - The pseudo-constructor `Color.new` has been removed. - Color class constructors no longer yield the constructed color if a block is passed. - Renamed `Color::COLOR_EPSILON` and `Color::COLOR_TOLERANCE` to `Color::EPSILON` and `Color::TOLERANCE`. These aren't private constants because they need to be accessed throughout Color, but they are _internal_ constants that should not be used outside of the Color library or functions exposed therein. - PDF format functions `#pdf_fill` and `#pdf_stroke` have been removed from `Color::CMYK`, `Color::Grayscale`, and `Color::RGB`. The supporting internal constants `Color::::PDF_FORMAT_STR` have also been removed. - Palette processing classes, `Color::Palette::AdobeColor`, `Color::Palette::Gimp`, and `Color::Palette::MonoContrast` have been removed. Persons interested in using these are encouraged to extract them from [Color 1.8][color-1.8] and adapt them to use Color 2.0 APIs. - CSS methods (`#css_rgb`, `#css_rgba`, `#css_hsl`, `#css_hsla`) have been replaced with `#css` on color classes that have CSS representations. The output of `#css` differs (Color 1.8 used the legacy CSS color formats; Color 2.0 uses modern CSS color formats). - `Color::GrayScale` has been renamed to `Color::Grayscale`. The alias constant `Color::GreyScale` has been removed. - The `#html` method has been removed from all color classes except Color::RGB. - Named RGB colors are no longer defined automatically, but must be loaded explicitly by requiring `color/rgb/colors`. This resolves [#30][issue-30]. The use of `Color::RGB#extract_colors`, `Color::RGB.by_hex`, `Color::RGB.by_name`, or `Color::RGB.by_css` will require `color/rgb/colors` automatically as they require the presence of the named colors. - `Color:CSS#[]` has been removed, as has the containing namespace. It has always been a shallow wrapper around `Color::RGB.by_name`. ### 🚀 New Features - `Color::CIELAB` and `Color::XYZ` namespaces have been added. Separate implementations were submitted by David Heitzman and @stiff (in [#8][pull-8] and [#11][pull-11]), but I have reworked the code substantially. These implementations were originally as `Color::LAB` and include a new contrast calculation using the ΔE\*00 algorithm. ### Internal - Updated project structure for how I manage Ruby libraries in 2025. This includes increased release security (MFA is required for all releases, automated releases are enabled), full GitHub Actions, Dependabot, Standard Ruby, and more. - Charles Nutter re-added JRuby support in CI. [#36][pull-36] ### Governance Color 2.0 and later requires that all contributions be signed-off attesting that the developer created the change and has the appropriate permissions or ownership to contribute it to this project under the licence terms. ## 1.8 / 2015-10-26 - Add an optional `alpha` parameter to all `#css` calls. Thanks to Luke Bennellick (@bennell) and Alexander Popov (@AlexWayfer) for independently implemented submissions. Merged from [#15][pull-15]. - Improve constant detection to prevent incorrectly identified name collisions with various other libraries such as Azure deployment tools. Based on work by Matthew Draper (@matthewd) in [#24][pull-24]. - Prevent `Color.equivalent?` comparisons from using non-Color types for comparison. Fix provided by Benjamin Guest (@bguest) in [#18][pull-18]. - This project now has a [Code of Conduct](CODE_OF_CONDUCT.md). ## 1.7.1 / 2014-06-12 - Renamed `Color::RGB::BeccaPurple` to `Color::RGB::RebeccaPurple` as stipulated by Eric Meyer. For purposes of backwards compatibility, the previous name is still permitted, but its use is strongly discouraged, and it will be removed in the Color 2.0 release. ## 1.7 / 2014-06-12 - Added `Color::RGB::BeccaPurple` ([#663399][gh-issue-663399]) in honour of Rebecca Meyer, the daughter of Eric Meyer, who passed away on 7 June 2014. Her favourite color was purple. `#663399becca` - Changed the homepage in the gem to point to GitHub instead of RubyForge, which has been shut down. Fixes [#10][issue-10], reported by [@voxik][gh-user-voxik]. ## 1.6 / 2014-05-19 - Aaron Hill ([@armahillo][gh-user-armahillo]) implemented the CIE Delta E 94 method by which an RGB color can be asked for the closest matching color from a list of provided colors. Fixes [#5][issue-5]. - To implement `#closest_match` and `#delta_e94`, conversion methods for sRGB to XYZ and XYZ to L\*a\*b\* space were implemented. These should be considered experimental. - Ensured that the gem manifest was up-to-date. Fixes [#4][issue-4] reported by @boutil. Thanks! - Fixed problems with Travis builds. Note that Ruby 1.9.2 is no longer tested. Rubinius remains in a 'failure-tolerated' mode. - Color 1.6 is, barring security patches, the last release of Color that will support Ruby 1.8. ## 1.5.1 / 2014-01-28 - color 1.5 was a yanked release. - Added new methods to `Color::RGB` to make it so that the default defined colors can be looked up by hex, name, or both. - Added a method to `Color::RGB` to extract colors from text by hex, name, or both. - Added new common methods for color names. Converted colors do not retain names. - Restructured color comparisons to use protocols instead of custom implementations. This makes it easier to implement new color classes. To make this work, color classes should `include` Color only need to implement `#coerce(other)`, `#to_a`, and supported conversion methods (e.g., `#to_rgb`). - Added [@daveheitzman][gh-user-daveheitzman]'s initial implementation of a RGB contrast method as an extension file: `require 'color/rgb/contrast'`. This method and the value it returns should be considered experimental; it requires further examination to ensure that the results produced are consistent with the contrast comparisons used in `Color::Palette::MonoContrast`. - Reducing duplicated code. - Moved `lib/color/rgb-colors.rb` to `lib/color/rgb/colors.rb`. - Improved the way that named colors are specified internally. - Fixed bugs with Ruby 1.8.7 that may have been introduced in color 1.4.2. - Added simplecov for test coverage analysis. - Modernized Travis CI support. ## 1.4.2 / 2013-06-30 - Modernized Hoe installation of Color, removing some dependencies. - Switched to Minitest. - Turned on Travis CI. - Started using Code Climate. - Small code formatting cleanup that touched pretty much every file. ## 1.4.1 / 2010-02-03 - Imported to GitHub. - Converted to Hoe 2.5 spec format. ## 1.4.0 / 2007-02-11 - Merged Austin Ziegler's color-tools library (previously part of the Ruby PDF Tools project) with Matt Lyon's color library. - The HSL implementation from the Color class has been merged into `Color::HSL`. Color is a module the way it was for color-tools. - A thin veneer has been written to allow Color::new to return a `Color::HSL` instance; `Color::HSL` supports as many methods as possible that were previously supported by the Color class. - Values that were previously rounded by Color are no longer rounded; fractional values matter. - Converted to hoe for project management. - Moved to the next step of deprecating `Color::` values; printing a warning for each use (see the history for color-tools 1.3.0). - Print a warning on the access of either `VERSION` or `COLOR_TOOLS_VERSION`; the version constant is now `COLOR_VERSION`. - Added humanized versions of accessors (e.g., CMYK colors now have both #cyan and #c to access the cyan component of the color; #cyan provides the value as a percentage). - Added CSS3 formatters for RGB, RGBA, HSL, and HSLA outputs. Note that the Color library does not yet have a way of setting alpha opacity, so the output for RGBA and HSLA are at full alpha opacity (1.0). The values are output with two decimal places. - Applied a patch to provide simple arithmetic color addition and subtraction to `Color::GrayScale` and `Color::RGB`. The patch was contributed by Jeremy Hinegardner. This patch also provides the ability to return the maximum RGB value as a grayscale color. - Fixed two problems reported by Jean Krohn against color-tools relating to RGB-to-HSL and HSL-to-RGB conversion. (Color and color-tools use the same formulas, but the ordering of the calculations is slightly different with Color and did not suffer from this problem; color-tools was more sensitive to floating-point values and precision errors.) - Fixed an issue with HSL/RGB conversions reported by Adam Johnson. - Added an Adobe Color swatch (Photoshop) palette reader, `Color::Palette::AdobeColor` (for `.aco` files only). ## Color 0.1.0 / 2006-08-05 - Added HSL (degree, percent, percent) interface. - Removed RGB instance variable; color is managed internally as HSL floating point. - Tests! ## color-tools 1.3.0 - Added new metallic colors suggested by Jim Freeze. These are in the namespace `Color::Metallic`. - Colours that were defined in the Color namespace (e.g., `Color::Red`, `Color::AliceBlue`) are now defined in Color::RGB (e.g., `Color::RGB::Red`, `Color::RGB::AliceBlue`). They are added back to the Color namespace on the first use of the old colors and a warning is printed. In version 1.4, this warning will be printed on every use of the old colors. In version 1.5, the backwards compatible support for colors like Color::Red will be removed completely. - Added the `Color::CSS` module that provides a name lookup of `Color::RGB`-namespace constants with `Color::CSS[name]`. Most of these colors (which are mirrored from the `Color::RGB` default colors) are only "officially" recognised under the CSS3 color module or SVG. - Added the `Color::HSL` color space and some helper utilities to `Color::RGB` for color manipulation using the HSL value. - Controlled internal value replacement to be between 0 and 1 for all colors. - Updated `Color::Palette::Gimp` to more meaningfully deal with duplicate named colors. Named colors now return an array of colors. - Indicated the plans for some methods and constants out to color-tools 2.0. - Added unit tests and fixed a number of hidden bugs because of them. ## color-tools 1.2.0 - Changed installer from a custom-written install.rb to setup.rb 3.3.1-modified. - Added `Color::GreyScale` (or `Color::GrayScale`). - Added `Color::YIQ`. This color definition is incomplete; it does not have conversions from YIQ to other color spaces. ## color-tools 1.1.0 - Added `color/palette/gimp` to support the reading and use of GIMP color palettes. ## color-tools 1.0.0 - Initial release. [color-1.8]: https://github.com/halostatue/color/tree/v1.8 [css-color]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color [css-device-cmyk]: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/device-cmyk [issue-10]: https://github.com/halostatue/color/issues/10 [issue-30]: https://github.com/halostatue/color/issues/30 [issue-45]: https://github.com/halostatue/color/issues/45 [issue-4]: https://github.com/halostatue/color/issues/4 [issue-5]: https://github.com/halostatue/color/issues/5 [issue-92]: https://github.com/halostatue/color/issues/92 [issue-95]: https://github.com/halostatue/color/issues/95 [pull-11]: https://github.com/halostatue/color/pull/11 [pull-15]: https://github.com/halostatue/color/pull/15 [pull-18]: https://github.com/halostatue/color/pull/18 [pull-24]: https://github.com/halostatue/color/pull/24 [pull-36]: https://github.com/halostatue/color/pull/36 [pull-46]: https://github.com/halostatue/color/pull/46 [pull-69]: https://github.com/halostatue/color/pull/69 [pull-8]: https://github.com/halostatue/color/pulls/8 [pull-93]: https://github.com/halostatue/color/pull/93 [pull-96]: https://github.com/halostatue/color/pull/96 [wp-std-illuminant]: https://en.wikipedia.org/wiki/Standard_illuminant#White_points_of_standard_illuminants [gh-user-akicho8]: https://github.com/akicho8 [gh-issue-89]: https://github.com/halostatue/color/issues/89 [gh-user-alexwlchan]: https://github.com/alexwlchan [gh-issue-663399]: https://github.com/halostatue/color/issues/663399 [gh-user-voxik]: https://github.com/voxik [gh-user-armahillo]: https://github.com/armahillo [gh-user-daveheitzman]: https://github.com/daveheitzman color-2.2.0/LICENCE.md0000644000004100000410000000404715137215736014264 0ustar www-datawww-data# Licence - SPDX-License-Identifier: [MIT][mit] - Copyright 2005-2025 Austin Ziegler, Matt Lyon, and other contributors. The software in this repository is made available under the MIT license. ## MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Developer Certificate of Origin All contributors **must** certify they are willing and able to provide their contributions under the terms of this project's licences with the certification of the [Developer Certificate of Origin (Version 1.1)](licences/dco.txt). Such certification is provided by ensuring that a `Signed-off-by` [commit trailer][trailer] is present on every commit: Signed-off-by: FirstName LastName The `Signed-off-by` trailer can be automatically added by git with the `-s` or `--signoff` option on `git commit`: ```sh git commit --signoff ``` [mit]: https://spdx.org/licenses/MIT.html [trailer]: https://git-scm.com/docs/git-interpret-trailers