class GSpotMichael

Constants

MAX_DARK_LUMA
MAX_MUTED_SATURATION
MAX_NORMAL_LUMA
MIN_LIGHT_LUMA
MIN_NORMAL_LUMA
MIN_VIBRANT_SATURATION
TARGET_DARK_LUMA
TARGET_LIGHT_LUMA
TARGET_MUTED_SATURATION
TARGET_NORMAL_LUMA
TARGET_VIBRANT_SATURATION
WEIGHT_LUMA
WEIGHT_POPULATION
WEIGHT_SATURATION

Public Class Methods

new(file, colorCount=64, quality='5%') click to toggle source
# File lib/g_spot_michael.rb, line 25
def initialize(file, colorCount=64, quality='5%')
  @HighestPopulation = 0

  # we're going to take the original file,
  # reduce the size, quantize the colors,
  # and then return an array of distinct
  # RGBs with their occurence counts(as pairs)
  file.rewind
  results = run_command "convert - -scale '#{quality}' +dither -colors #{colorCount} txt:", file.read.force_encoding("UTF-8")
  pixels  = results.split("\n").map{|x| (r = x.match(/srgb\((\d{1,3},\d{1,3},\d{1,3})\)/)) ? r[1] : nil}.compact.map{|p| p.split(",").map(&:to_i)}
  cmap    = {}
  pixels.each do |pixel|
    # If pixel is mostly opaque and not white
    if !(pixel[0] > 250 && pixel[1] > 250 && pixel[2] > 250)
      cmap[pixel] ||= 0
      cmap[pixel] += 1
    end
  end
  
  @swatches = cmap.map do |vbox|
    Swatch.new vbox[0], vbox[1]  # values, count
  end

  @maxPopulation     = find_max_population
  @HighestPopulation = @maxPopulation

  generate_variation_colors
  generate_empty_swatches
end

Public Instance Methods

create_comparison_value(saturation, targetSaturation, luma, targetLuma, population, maxPopulation) click to toggle source
# File lib/g_spot_michael.rb, line 110
def create_comparison_value(saturation, targetSaturation, luma, targetLuma, population, maxPopulation)
  # pop = (maxPopulation != 0) ? (population / maxPopulation) : 0.0
  weighted_mean(
    invert_diff(saturation, targetSaturation), WEIGHT_SATURATION,
    invert_diff(luma, targetLuma), WEIGHT_LUMA,
    (population / maxPopulation), WEIGHT_POPULATION
  )
end
find_color_variation(targetLuma, minLuma, maxLuma, targetSaturation, minSaturation, maxSaturation) click to toggle source
# File lib/g_spot_michael.rb, line 91
def find_color_variation (targetLuma, minLuma, maxLuma, targetSaturation, minSaturation, maxSaturation)
  max      = nil
  maxValue = 0
  
  @swatches.each do |swatch|
    sat  = swatch.hsl[1]
    luma = swatch.hsl[2]
    if !is_already_selected(swatch) && sat >= minSaturation && sat <= maxSaturation && luma >= minLuma && luma <= maxLuma
      value = create_comparison_value sat, targetSaturation, luma, targetLuma, swatch.population, @HighestPopulation
      if !max || value > maxValue
        max      = swatch
        maxValue = value
      end
    end
  end

  max
end
find_max_population() click to toggle source
# File lib/g_spot_michael.rb, line 86
def find_max_population
  population = @swatches.map{|swatch| [0, swatch.population].max }.max
  population
end
generate_empty_swatches() click to toggle source
# File lib/g_spot_michael.rb, line 64
def generate_empty_swatches
  if !@VibrantSwatch
    # If we do not have a vibrant color...
    if @DarkVibrantSwatch
      # ...but we do have a dark vibrant, generate the value by modifying the luma
      hsl    = @DarkVibrantSwatch.hsl
      hsl[2] = TARGET_NORMAL_LUMA
      @VibrantSwatch = Swatch.new hslToRgb(hsl[0], hsl[1], hsl[2]), 0
    end
  end

  if !@DarkVibrantSwatch
    # If we do not have a vibrant color...
    if @VibrantSwatch
      # ...but we do have a dark vibrant, generate the value by modifying the luma
      hsl    = @VibrantSwatch.hsl
      hsl[2] = TARGET_DARK_LUMA
      @DarkVibrantSwatch = Swatch.new hslToRgb(hsl[0], hsl[1], hsl[2]), 0
    end
  end
end
generate_variation_colors() click to toggle source
# File lib/g_spot_michael.rb, line 55
def generate_variation_colors
  @VibrantSwatch      = find_color_variation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
  @LightVibrantSwatch = find_color_variation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
  @DarkVibrantSwatch  = find_color_variation(TARGET_DARK_LUMA, 0, MAX_DARK_LUMA, TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1)
  @MutedSwatch        = find_color_variation(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
  @LightMutedSwatch   = find_color_variation(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
  @DarkMutedSwatch    = find_color_variation(TARGET_DARK_LUMA, 0, MAX_DARK_LUMA, TARGET_MUTED_SATURATION, 0, MAX_MUTED_SATURATION)
end
hslToRgb(h, s, l) click to toggle source
# File lib/g_spot_michael.rb, line 157
def hslToRgb(h, s, l)
  r = nil
  g = nil
  b = nil

  hue2rgb = Proc.new { |p, q, t|
    if t < 0
      t += 1
    end
    if t > 1
      t -= 1
    end
    if t < 1 / 6
      return p + (q - p) * 6 * t
    end
    if t < 1 / 2
      return q
    end
    if t < 2 / 3
      return p + (q - p) * (2 / 3 - t) * 6
    end
    p
  }

  if s == 0
    r = g = b = l
    # achromatic
  else
    q = (l < 0.5) ? (l * (1 + s)) : (l + s - (l * s))
    p = 2 * l - q
    r = hue2rgb.call(p, q, h + 1 / 3)
    g = hue2rgb.call(p, q, h)
    b = hue2rgb.call(p, q, h - (1 / 3))
  end
  [
    r * 255,
    g * 255,
    b * 255
  ]
end
invert_diff(value, targetValue) click to toggle source
# File lib/g_spot_michael.rb, line 119
def invert_diff (value, targetValue)
  1 - (value - targetValue).abs
end
is_already_selected(swatch) click to toggle source
# File lib/g_spot_michael.rb, line 148
def is_already_selected(swatch)
  @VibrantSwatch == swatch || 
  @DarkVibrantSwatch == swatch ||
  @LightVibrantSwatch == swatch || 
  @MutedSwatch == swatch ||
  @DarkMutedSwatch == swatch || 
  @LightMutedSwatch == swatch
end
run_command(command, input) click to toggle source
# File lib/g_spot_michael.rb, line 198
def run_command command, input
  stdin, stdout, stderr, wait_thr = Open3.popen3(command)
  pid = wait_thr.pid

  Timeout.timeout(10) do # cancel in 10 seconds
    stdin.write input
    stdin.close

    output_buffer = []
    error_buffer  = []

    while (output_chunk = stdout.gets) || (error_chunk = stderr.gets)
      output_buffer << output_chunk
      error_buffer  << error_chunk
    end

    output_buffer.compact!
    error_buffer.compact!

    output = output_buffer.any? ? output_buffer.join('') : nil
    error  = error_buffer.any?  ? error_buffer.join('')  : nil

    unless error
      raise StandardError.new("No output received.") if !output
      return output
    else
      raise StandardError.new(error)
    end
  end
rescue Timeout::Error, StandardError, Errno::EPIPE => e
  e
ensure
  begin
    Process.kill("KILL", pid) if pid
  rescue Errno::ESRCH
    # Process is already dead so do nothing.
  end
  stdin  = nil
  stdout = nil
  stderr = nil
  wait_thr.value if wait_thr # Process::Status object returned.
end
swatches() click to toggle source
# File lib/g_spot_michael.rb, line 137
def swatches
  {
    Vibrant: @VibrantSwatch,
    Muted: @MutedSwatch,
    DarkVibrant: @DarkVibrantSwatch,
    DarkMuted: @DarkMutedSwatch,
    LightVibrant: @LightVibrantSwatch,
    LightMuted: @LightMutedSwatch
  }
end
weighted_mean(*values) click to toggle source
# File lib/g_spot_michael.rb, line 123
def weighted_mean(*values)
  sum = 0
  sumWeight = 0
  i = 0
  while i < values.length
    value = values[i]
    weight = values[i + 1]
    sum += value * weight
    sumWeight += weight
    i += 2
  end
  sum / sumWeight
end