halostatue/color

View on GitHub
lib/color/palette/adobecolor.rb

Summary

Maintainability
D
1 day
Test Coverage
require 'color/palette'

# A class that can read an Adobe Color palette file (used for Photoshop
# swatches) and provide a Hash-like interface to the contents. Not all
# colour formats in ACO files are supported. Based largely off the
# information found by Larry Tesler[http://www.nomodes.com/aco.html].
#
# Not all Adobe Color files have named colours; all named entries are
# returned as an array.
#
#   pal = Color::Palette::AdobeColor.from_file(my_aco_palette)
#   pal[0]          => Color::RGB<...>
#   pal["white"]    => [ Color::RGB<...> ]
#   pal["unknown"]  => [ Color::RGB<...>, Color::RGB<...>, ... ]
#
# AdobeColor palettes are always indexable by insertion order (an integer
# key).
#
# Version 2 palettes use UTF-16 colour names.
class Color::Palette::AdobeColor
  include Enumerable

  class << self
    # Create an AdobeColor palette object from the named file.
    def from_file(filename)
      File.open(filename, "rb") { |io| Color::Palette::AdobeColor.from_io(io) }
    end

    # Create an AdobeColor palette object from the provided IO.
    def from_io(io)
      Color::Palette::AdobeColor.new(io.read)
    end
  end

  # Returns statistics about the nature of the colours loaded.
  attr_reader :statistics
  # Contains the "lost" colours in the palette. These colours could not be
  # properly loaded (e.g., L*a*b* is not supported by Color, so it is
  # "lost") or are not understood by the algorithms.
  attr_reader :lost

  # Use this to convert the unsigned word to the signed word, if necessary.
  UwToSw = proc { |n| (n >= (2 ** 16)) ? n - (2 ** 32) : n } #:nodoc:

  # Create a new AdobeColor palette from the palette file as a string.
  def initialize(palette)
    @colors     = []
    @names      = {}
    @statistics = Hash.new(0)
    @lost       = []
    @order      = []
    @version    = nil

    class << palette
      def readwords(count = 1)
        @offset ||= 0
        raise IndexError if @offset >= self.size
        val = self[@offset, count * 2]
        raise IndexError if val.nil? or val.size < (count * 2)
        val = val.unpack("n" * count)
        @offset += count * 2
        val
      end

      def readutf16(count = 1)
        @offset ||= 0
        raise IndexError if @offset >= self.size
        val = self[@offset, count * 2]
        raise IndexError if val.nil? or val.size < (count * 2)
        @offset += count * 2
        val
      end
    end

    @version, count = palette.readwords 2

    raise "Unknown AdobeColor palette version #@version." unless @version.between?(1, 2)

    count.times do
      space, w, x, y, z = palette.readwords 5
      name = nil
      if @version == 2
        raise IndexError unless palette.readwords == [ 0 ]
        len = palette.readwords
        name = palette.readutf16(len[0] - 1)
        raise IndexError unless palette.readwords == [ 0 ]
      end

      color = case space
              when 0 then # RGB
                @statistics[:rgb] += 1

                Color::RGB.new(w / 256, x / 256, y / 256)
              when 1 then # HS[BV] -- Convert to RGB
                @statistics[:hsb] += 1

                h = w / 65535.0
                s = x / 65535.0
                v = y / 65535.0

                if defined?(Color::HSB)
                  Color::HSB.from_fraction(h, s, v)
                else
                  @statistics[:converted] += 1
                  if Color.near_zero_or_less?(s)
                    Color::RGB.from_fraction(v, v, v)
                  else
                    if Color.near_one_or_more?(h)
                      vh = 0
                    else
                      vh = h * 6.0
                    end

                    vi = vh.floor
                    v1 = v.to_f * (1 - s.to_f)
                    v2 = v.to_f * (1 - s.to_f * (vh - vi))
                    v3 = v.to_f * (1 - s.to_f * (1 - (vh - vi)))

                    case vi
                    when 0 then Color::RGB.from_fraction(v, v3, v1)
                    when 1 then Color::RGB.from_fraction(v2, v, v1)
                    when 2 then Color::RGB.from_fraction(v1, v, v3)
                    when 3 then Color::RGB.from_fraction(v1, v2, v)
                    when 4 then Color::RGB.from_fraction(v3, v1, v)
                    else Color::RGB.from_fraction(v, v1, v2)
                    end
                  end
                end
              when 2 then # CMYK
                @statistics[:cmyk] += 1
                Color::CMYK.from_percent(100 - (w / 655.35),
                                         100 - (x / 655.35),
                                         100 - (y / 655.35),
                                         100 - (z / 655.35))
              when 7 then # L*a*b*
                @statistics[:lab] += 1

                l = [w, 10000].min / 100.0
                a = [[-12800, UwToSw[x]].max, 12700].min / 100.0
                b = [[-12800, UwToSw[x]].max, 12700].min / 100.0

                if defined? Color::Lab
                  Color::Lab.new(l, a, b)
                else
                  [ space, w, x, y, z ]
                end
              when 8 then # Grayscale
                @statistics[:gray] += 1

                g = [w, 10000].min / 100.0
                Color::GrayScale.new(g)
              when 9 then # Wide CMYK
                @statistics[:wcmyk] += 1

                c = [w, 10000].min / 100.0
                m = [x, 10000].min / 100.0
                y = [y, 10000].min / 100.0
                k = [z, 10000].min / 100.0
                Color::CMYK.from_percent(c, m, y, k)
              else
                @statistics[space] += 1
                [ space, w, x, y, z ]
              end

      @order << [ color, name ]

      if color.kind_of? Array
        @lost << color
      else
        @colors << color

        if name
          @names[name] ||= []
          @names[name] << color
        end
      end
    end
  end

  # Provides the colour or colours at the provided selectors.
  def values_at(*selectors)
    @colors.values_at(*selectors)
  end

  # If a Numeric +key+ is provided, the single colour value at that position
  # will be returned. If a String +key+ is provided, the colour set (an
  # array) for that colour name will be returned.
  def [](key)
    if key.kind_of?(Numeric)
      @colors[key]
    else
      @names[key]
    end
  end

  # Loops through each colour.
  def each
    @colors.each { |el| yield el }
  end

  # Loops through each named colour set.
  def each_name #:yields color_name, color_set:#
    @names.each { |color_name, color_set| yield color_name, color_set }
  end

  def size
    @colors.size
  end

  attr_reader :version

  def to_aco(version = @version) #:nodoc:
    res = ""

    res << [ version, @order.size ].pack("nn")

    @order.each do |cnpair|
      color, name = *cnpair

      # Note: HSB and CMYK formats are lost by the conversions performed on
      # import. They are turned into RGB and WCMYK, respectively.

      cstr = case color
             when Array
               color
             when Color::RGB
               r = [(color.red * 256).round, 65535].min
               g = [(color.green * 256).round, 65535].min
               b = [(color.blue * 256).round, 65535].min
               [ 0, r, g, b, 0 ]
             when Color::GrayScale
               g = [(color.gray * 100).round, 10000].min
               [ 8, g, 0, 0, 0 ]
             when Color::CMYK
               c = [(color.cyan * 100).round, 10000].min
               m = [(color.magenta * 100).round, 10000].min
               y = [(color.yellow * 100).round, 10000].min
               k = [(color.black * 100).round, 10000].min
               [ 9, c, m, y, k ]
             end
      cstr = cstr.pack("nnnnn")

      nstr = ""

      if version == 2
        if (name.size / 2 * 2) == name.size # only where s[0] == byte!
          nstr << [ 0, (name.size / 2) + 1 ].pack("nn")
          nstr << name
          nstr << [ 0 ].pack("n")
        else
          nstr << [ 0, 1, 0 ].pack("nnn")
        end
      end

      res << cstr << nstr
    end

    res
  end
end