Module: Tessel::Quantize

Defined in:
lib/tessel/quantize.rb,
sig/tessel.rbs

Class Method Summary collapse

Class Method Details

.fixed_palette ⇒ Array[[Integer, Integer, Integer]]

Returns:

  • (Array[[Integer, Integer, Integer]])


39
40
41
42
43
# File 'lib/tessel/quantize.rb', line 39

def fixed_palette
  (0..5).flat_map do |r|
    (0..5).flat_map { |g| (0..5).map { |b| [r * 51, g * 51, b * 51] } }
  end.concat((0...40).map { |n| value = (n * 255.0 / 39).round; [value, value, value] })
end

.palette_for(images, colors: 256, sample_limit: 100_000) ⇒ Array[[Integer, Integer, Integer]]

Parameters:

  • (Image, Array[Image])
  • colors: (Integer) (defaults to: 256)
  • sample_limit: (Integer) (defaults to: 100_000)

Returns:

  • (Array[[Integer, Integer, Integer]])

Raises:

  • (ArgumentError)


15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/tessel/quantize.rb', line 15

def palette_for(images, colors: 256, sample_limit: 100_000)
  images = [images] unless images.is_a?(Array)
  raise ArgumentError, "sample_limit must be positive" unless sample_limit.is_a?(Integer) && sample_limit.positive?
  validate_colors(colors)
  total = images.sum do |image|
    raise TypeError, "images must contain Tessel::Image values" unless image.is_a?(Image)

    image.width * image.height
  end
  raise ArgumentError, "images must not be empty" if total.zero?

  stride = [(total.to_f / sample_limit).ceil, 1].max
  histogram = Hash.new(0)
  position = 0
  images.each do |image|
    image.bytes.bytes.each_slice(4) do |r, g, b, a|
      histogram[[r, g, b]] += 1 if (position % stride).zero? && a.positive?
      position += 1
    end
  end
  histogram[[0, 0, 0]] = 1 if histogram.empty?
  median_cut(histogram, colors)
end

.quantize(image, colors: 256, palette: nil, dither: :none, serpentine: true) ⇒ [String, Array[[Integer, Integer, Integer]]]

Parameters:

  • (Image)
  • colors: (Integer) (defaults to: 256)
  • palette: (Array[[Integer, Integer, Integer]], nil) (defaults to: nil)
  • dither: (Symbol) (defaults to: :none)
  • serpentine: (Boolean) (defaults to: true)

Returns:

  • ([String, Array[[Integer, Integer, Integer]]])

Raises:

  • (TypeError)


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/tessel/quantize.rb', line 49

def quantize(image, colors: 256, palette: nil, dither: :none, serpentine: true)
  raise TypeError, "image must be a Tessel::Image" unless image.is_a?(Image)
  raise TypeError, "palette must be an array" if palette && !palette.is_a?(Array)
  raise ArgumentError, "palette must not be empty" if palette && palette.empty?
  raise ArgumentError, "unknown dither: #{dither}" unless i[none floyd_steinberg ordered].include?(dither)

  palette ||= palette_for(image, colors: colors)
  palette = palette.map do |color|
    raise TypeError, "palette colors must be RGB triples" unless color.is_a?(Array) && color.length == 3 && color.all? { |v| v.is_a?(Integer) && v.between?(0, 255) }
    color
  end
  raise ArgumentError, "palette must contain at most 256 colors" if palette.length > 256

  nearest = {}
  indices = "\0".b * (image.width * image.height)
  errors = Array.new(image.width) { [0, 0, 0] }
  next_errors = Array.new(image.width) { [0, 0, 0] }
  bytes = image.bytes
  image.height.times do |y|
    reverse = dither == :floyd_steinberg && serpentine && y.odd?
    scan = reverse ? (image.width - 1).downto(0) : (0...image.width)
    scan.each do |x|
      index = y * image.width + x
      offset = index * 4
      r, g, b, alpha = bytes.getbyte(offset), bytes.getbyte(offset + 1), bytes.getbyte(offset + 2), bytes.getbyte(offset + 3)
      if dither == :floyd_steinberg && alpha.positive?
        input = [r, g, b].each_with_index.map { |v, c| [[v + errors[x][c].div(16), 0].max, 255].min }
      elsif dither == :ordered
        threshold = ((MATRIX[y % 8][x % 8] * 2 - 63) * 4).div(16)
        input = [r, g, b].map { |v| [[v + threshold, 0].max, 255].min }
      else
        input = [r, g, b]
      end
      key = input.pack("C3")
      palette_index = nearest[key] ||= closest_index(input, palette)
      indices.setbyte(index, palette_index)

      if dither == :floyd_steinberg && alpha.positive?
        color = palette[palette_index]
        direction = reverse ? -1 : 1
        distribute(errors, next_errors, x, direction, input, color)
      end
      if x == (reverse ? 0 : image.width - 1)
        errors, next_errors = next_errors, Array.new(image.width) { [0, 0, 0] }
      end
    end
  end
  [indices, palette]
end

.web_safe_palette ⇒ Array[[Integer, Integer, Integer]]

Returns:

  • (Array[[Integer, Integer, Integer]])


45
46
47
# File 'lib/tessel/quantize.rb', line 45

def web_safe_palette
  [0, 51, 102, 153, 204, 255].repeated_permutation(3).to_a
end