Class: Gifenc::ColorTable

Inherits:
Object
  • Object
show all
Defined in:
lib/color_table.rb

Overview

The color table is the palette of the GIF, it contains all the colors that may appear in any of its images. The color table can be global (GCT), in which case it applies to all images, and local (LCT), in which case it applies only to the subsequent image, overriding the global one, if present. Technically, both are optional according to the standard, but this breaks many decoders, so by default we enforce having an LCT if no GCT is present.

A color table can have a size of at most 8 bits, i.e., at most 256 colors, and it's always a power of 2, even if there's leftover space or empty slots. The color of each pixel in the image is then determined by specifying its index in the corresponding color table (local, if present, or global). Regardless of the bit depth, each color component still takes up a byte (and each pixel thus 3 bytes) in the encoded GIF file.

This class handles all the logic dealing with color indexes (in the table) internally, so that the user can work purely with colors directly.

Notes:

  • Many of the methods that manipulate the color table return the table back, so that they may be chained properly.
  • Several methods may change the color indexes, thus potentially corrupting images already made with this table. These methods are indicated with a note, and should probably only be used when building the desired palette, before actually starting to use it to craft images.

Constant Summary collapse

MAX_SIZE =

The maximum size of a GIF color table. The encoder will always choose the smallest size possible that fits all colors, so this is only a hard limit.

256

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(colors = [], depth: 8, sorted: false) ⇒ ColorTable

Creates a new color table. This color table can then be used as a GCT, as an LCT for as many images as desired, or both.

Parameters:

  • colors (Array<Integer>) (defaults to: [])

    An ordered list of colors to initialize the table with. Colors can be duplicated, but regardless, the list should be at most 256 colors long. Since the indexes matter, the list may contain nils, which represents empty slots in the table.

  • depth (Integer) (defaults to: 8)

    Specifies the bit depth (1-8) for each color component in the original image. This does not set the actual GIF's color depth (that is always 8), and is ignored by most decoders.

  • sorted (Boolean) (defaults to: false)

    Indicates that the colors in the table are sorted by importance. It's essentially a deprecated flag that most decoders ignore.



69
70
71
72
73
74
# File 'lib/color_table.rb', line 69

def initialize(colors = [], depth: 8, sorted: false)
  clear
  @depth = depth.clamp(1, 8)
  @sorted = sorted
  set(colors)
end

Instance Attribute Details

#colorsArray<Integer> (readonly)

The raw list of colors in the table. May contain nils, which represents empty slots in the table (since the exact indexes matter). To change the color list in bulk, use the #set method. To change individual colors, use the #replace method.

Returns:

  • (Array<Integer>)

    The raw list of colors.



55
56
57
# File 'lib/color_table.rb', line 55

def colors
  @colors
end

#depthInteger

Note:

This attribute is essentially meaningless nowadays, and ignored by most decoders.

The color resolution, in bits per channel, of the original image, NOT of the GIF. The GIF's color depth is always 8 bits per channel.

Returns:

  • (Integer)

    Original color bit depth.



39
40
41
# File 'lib/color_table.rb', line 39

def depth
  @depth
end

#sortedBoolean

Note:

This attribute is essentially meaningless nowadays, and ignored by most decoders.

Whether the colors of the table are sorted in decreasing order of importance. As per the specification, this would typically be decreasing order of frequency, in order to assist older systems and decoders with fewer available colors in choosing the best subset of colors to represent the image.

Returns:

  • (Boolean)

    Whether the table colors are sorted or not.



48
49
50
# File 'lib/color_table.rb', line 48

def sorted
  @sorted
end

Instance Method Details

#add(*colors) ⇒ ColorTable

Insert new colors into the color table.

Parameters:

  • colors (Integers)

    The colors to add.

Returns:

Raises:



226
227
228
229
230
231
232
233
234
# File 'lib/color_table.rb', line 226

def add(*colors)
  colors = (colors - @colors).compact.uniq
  if count + colors.size > MAX_SIZE
    raise Exception::ColorTableError, "Cannot add colors to the color table:\
      Table over size limit (#{MAX_SIZE})."
  end
  colors.each{ |c| @colors[find_slot] = c & 0xFFFFFF }
  self
end

#clearColorTable Also known as: reset

Note:

This method may change color indexes.

Empties the whole color table, bringing its size down to 0.

Returns:



163
164
165
166
# File 'lib/color_table.rb', line 163

def clear
  @colors = [nil] * MAX_SIZE
  self
end

#compactColorTable

Note:

This method may change color indexes.

Rearrange all colors to remove empty intermediate slots. This is accomplished by laying out all colors subsequently from the start. The order of the actual colors is preserved.

Returns:

See Also:



143
144
145
146
147
# File 'lib/color_table.rb', line 143

def compact
  @colors.compact!
  @colors += [nil] * (MAX_SIZE - @colors.size)
  self
end

#countInteger

Count of actual colors present in the table.

Returns:

  • (Integer)

    Color count.

See Also:



282
283
284
# File 'lib/color_table.rb', line 282

def count
  @colors.count{ |c| !!c }
end

#cycle(*colors, step: 1) ⇒ ColorTable

Rearrange a subset of colors in the table according to a cycle. For example, if we cycle the colors A, B, C, D with a step of -2, the colors become C, D, A, B.

Parameters:

  • colors (Integers)

    The sequence of colors to shift in a cycle.

  • step (Integer) (defaults to: 1)

    The positive or negative step to take in the shift.

Returns:

Raises:

See Also:



206
207
208
209
# File 'lib/color_table.rb', line 206

def cycle(*colors, step: 1)
  permutation = colors.times.map{ |i| (i - step) % colors.size }
  permute(*colors, order: permutation)
end

#delete(*colors) ⇒ ColorTable

Delete colors from the color table.

Parameters:

  • colors (Integers)

    The colors to delete.

Returns:



239
240
241
242
# File 'lib/color_table.rb', line 239

def delete(*colors)
  colors.each{ |c| replace(c, nil) }
  self
end

#distinctInteger

Count of distinct colors present in the table. See #count.

Returns:

  • (Integer)

    Distinct color count.

See Also:



289
290
291
# File 'lib/color_table.rb', line 289

def distinct
  @colors.uniq.count{ |c| !!c }
end

#dupColorTable

Create a duplicate copy of this color table.

Returns:



87
88
89
# File 'lib/color_table.rb', line 87

def dup
  ColorTable.new(@colors.dup, depth: @depth, sorted: @sorted)
end

#encode(stream) ⇒ Object

Encode the color table as it will appear in the GIF.

Parameters:

  • stream (IO)

    The stream to output the encoded color table into.



78
79
80
81
82
83
# File 'lib/color_table.rb', line 78

def encode(stream)
  @colors.take(size).each{ |c|
    c = 0 if !c
    stream << [c >> 16 & 0xFF, c >> 8 & 0xFF, c & 0xFF].pack('C3')
  }
end

#invertColorTable

Inverts all the colors in the table.

Returns:



256
257
258
259
# File 'lib/color_table.rb', line 256

def invert
  @colors.each{ |c| replace(c, c ^ 0xFFFFFF) }
  self
end

#permute(*colors, order: []) ⇒ ColorTable

Rearrange a subset of colors in the table according to a permutation. The permutation must match the length of the provided colors. For example, if we pass the colors A, B, C, D and the permutation [2, 0, 3, 1], the colors will now be sorted like C, A, D, B.

Parameters:

  • colors (Integers)

    The colors to rearrange.

  • order (Array<Integer>) (defaults to: [])

    The permutation according to which to rearrange.

Returns:

Raises:

  • (Exception::ColorTableError)

    If the permutation is invalid (e.g. not of the right length, or not containing the right indices), or if any color was not found in the color table.

See Also:



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/color_table.rb', line 181

def permute(*colors, order: [])
  # Ensure permutation makes sense for the provided colors
  if order.sort != colors.size.times.to_a || order.uniq.size != order.size
    raise Exception::ColorTableError, "Cannot permute colors: Permutation is invalid."
  end

  # Ensure provided colors are in the table
  if !(colors - @colors).empty?
    raise Exception::ColorTableError, "Cannot permute colors: Color not found."
  end

  mapping = colors.each_with_index.map{ |c, i| [c, colors[order[i]]] }.to_h
  @colors.map!{ |c| mapping[c] || c }
  
  self
end

#replace(old_color, new_color) ⇒ ColorTable

Changes one color in the table to another one.

Parameters:

  • old_color (Integer)

    The color to replace.

  • new_color (Integer)

    The color to change it to.

Returns:



248
249
250
251
252
# File 'lib/color_table.rb', line 248

def replace(old_color, new_color)
  new_color = !!new_color ? new_color & 0xFFFFFF : nil
  @colors.map!{ |c| c == old_color ? new_color : c }
  self
end

#set(colors) ⇒ ColorTable

Note:

This method may change color indexes.

Change all colors in this table with a different list of colors. The list may contain nils, indicating empty slots.

Parameters:

  • colors (Array<Integer>)

    The new list of colors.

Returns:

Raises:



109
110
111
112
113
114
115
116
# File 'lib/color_table.rb', line 109

def set(colors)
  if colors.size > MAX_SIZE
    raise Exception::ColorTableError, "Cannot build color table, the supplied color list\
      has more than #{MAX_SIZE} entries."
  end
  colors.each_with_index{ |c, i| @colors[i] = !!c ? c & 0xFFFFFF : nil }
  self
end

#simplifyColorTable

Note:

This method may change color indexes.

Simplifies the color table by removing color duplicates and empty slots. It's equivalent to uniq + compact.

Returns:

See Also:



155
156
157
158
# File 'lib/color_table.rb', line 155

def simplify
  uniq
  compact
end

#sizeInteger Also known as: length

Real size of the table that will be used by the encoder. The size is the smallest power of 2 capable of holding all colors currently in the list. It must be at least 2, even if there's a single color in the table.

Returns:

  • (Integer)

    Size of the table.

See Also:



272
273
274
# File 'lib/color_table.rb', line 272

def size
  2 ** bit_size
end

#swap(col_a, col_b) ⇒ ColorTable

Swap 2 colors of the color table. This can be used to change the theme of a GIF in a trivial way.

Parameters:

  • col_a (Integer)

    First color to swap.

  • col_b (Integer)

    Second color to swap.

Returns:

Raises:



217
218
219
# File 'lib/color_table.rb', line 217

def swap(col_a, col_b)
  permute(col_a, col_b, order: [1, 0])
end

#uniqColorTable

Note:

This method may change color indexes.

Eliminates duplicate colors from the color table. This will keep the first instance of each color untouched (i.e., its index will remain valid), and set subsequent duplicate entries to nil.

Returns:

See Also:



124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/color_table.rb', line 124

def uniq
  unique_colors = Set.new
  for i in (0 ... MAX_SIZE)
    next if !@colors[i]
    if unique_colors.include?(@colors[i])
      @colors[i] = nil
    else
      unique_colors.add(@colors[i])
    end
  end
  self
end