gifenc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/image.rb ADDED
@@ -0,0 +1,512 @@
1
+ require 'lzwrb'
2
+
3
+ module Gifenc
4
+ # Represents a single image. A GIF may contain multiple images, and they need
5
+ # not be animation frames (they could simply be tiles of a static image).
6
+ # Crucially, images can be smaller than the GIF logical screen (canvas), thus
7
+ # being placed at an offset of it, saving space and time, and allowing for more
8
+ # complex compositions. How each image interacts with the previous ones depends
9
+ # on properties like the disposal method ({#disposal}) and the transparency
10
+ # ({#trans_color}).
11
+ #
12
+ # Most methods modifying the image return the image itself, so that they can
13
+ # be chained properly.
14
+ class Image
15
+
16
+ # Width of the image in pixels. Use the {#resize} method to change it.
17
+ # @return [Integer] Image width.
18
+ # @see #resize
19
+ attr_reader :width
20
+
21
+ # Height of the image in pixels. Use the {#resize} method to change it.
22
+ # @return [Integer] Image height.
23
+ # @see #resize
24
+ attr_reader :height
25
+
26
+ # The image's horizontal offset in the GIF's logical screen. Note that the
27
+ # image will be cropped if it overflows the logical screen's boundary.
28
+ # @return [Integer] Image X offset.
29
+ # @see #move
30
+ # @see #place
31
+ attr_accessor :x
32
+
33
+ # The image's vertical offset in the GIF's logical screen. Note that the
34
+ # image will be cropped if it overflows the logical screen's boundary.
35
+ # @return [Integer] Image Y offset.
36
+ # @see #move
37
+ # @see #place
38
+ attr_accessor :y
39
+
40
+ # Default color of the canvas. This is the initial color of the image, as
41
+ # well as the color that appears in the new regions when the canvas is
42
+ # is enlarged.
43
+ # @return [Integer] Index of the canvas color in the color table.
44
+ attr_accessor :color
45
+
46
+ # The local color table to use for this image. If left unspecified (`nil`),
47
+ # the global color table will be used.
48
+ # @return [ColorTable] Local color table.
49
+ attr_accessor :lct
50
+
51
+ # Contains the table based image data (the color indexes for each pixel).
52
+ # Use the {#replace} method to bulk change the pixel data.
53
+ # @return [Array<Integer>] Pixel data.
54
+ # @see #replace
55
+ attr_reader :pixels
56
+
57
+ # Create a new image or frame. The minimum information required is the
58
+ # width and height, which may be supplied directly, or by providing the
59
+ # bounding box, which also contains the offset of the image in the
60
+ # logical screen.
61
+ # @param width [Integer] Width of the image in pixels.
62
+ # @param height [Integer] Height of the image in pixels.
63
+ # @param x [Integer] Horizontal offset of the image in the logical screen.
64
+ # @param y [Integer] Vertical offset of the image in the logical screen.
65
+ # @param bbox [Array<Integer>] The image's bounding box, which is a tuple
66
+ # in the form `[X, Y, W, H]`, where `[X, Y]` are the coordinates of its
67
+ # upper left corner, and `[W, H]` are its width and height, respectively.
68
+ # This can be provided instead of the first 4 parameters.
69
+ # @param color [Integer] The initial color of the canvas.
70
+ # @param gce [Extension::GraphicControl] An optional {Extension::GraphicControl
71
+ # Graphic Control Extension} for the image. This extension controls mainly
72
+ # 3 things: the image's *delay* onscreen, the color to use for
73
+ # *transparency*, and the *disposal* method to employ before displaying
74
+ # the next image. These things can instead be supplied individually in their
75
+ # corresponding parameters: `delay`, `trans_color` and `disposal`. Each
76
+ # individually passed parameter will override the corresponding value in
77
+ # the GCE, if supplied. If neither a GCE nor any of the 3 individual
78
+ # parameters is used, then a GCE will not be built, unless the attributes
79
+ # are written to later.
80
+ # @param delay [Integer] Time, in 1/100ths of a second, to wait before
81
+ # displaying the next image (see {#delay} for details).
82
+ # @param trans_color [Integer] Index of the color to use for transparency
83
+ # (see {#trans_color} for details)
84
+ # @param disposal [Integer] The disposal method to use after displaying
85
+ # this image and before displaying the next one (see {#disposal} for details).
86
+ # @param interlace [Boolean] Whether the pixel data of this image is
87
+ # interlaced or not.
88
+ # @param lct [ColorTable] Add a Local Color Table to this image, overriding
89
+ # the global one.
90
+ # @return [Image] The image.
91
+ def initialize(
92
+ width = nil,
93
+ height = nil,
94
+ x = nil,
95
+ y = nil,
96
+ bbox: nil,
97
+ color: DEFAULT_COLOR,
98
+ gce: nil,
99
+ delay: nil,
100
+ trans_color: nil,
101
+ disposal: nil,
102
+ interlace: DEFAULT_INTERLACE,
103
+ lct: nil
104
+ )
105
+ # Image attributes
106
+ if bbox
107
+ @x = bbox[0]
108
+ @y = bbox[1]
109
+ @width = bbox[2]
110
+ @height = bbox[3]
111
+ end
112
+ @width = width if width
113
+ @height = height if height
114
+ @x = x if x
115
+ @y = y if y
116
+ @lct = lct
117
+ @interlace = interlace
118
+
119
+ # Checks
120
+ raise Exception::CanvasError, "The width of the image must be supplied" if !@width
121
+ raise Exception::CanvasError, "The height of the image must be supplied" if !@height
122
+ @x = 0 if !@x
123
+ @y = 0 if !@y
124
+
125
+ # Image data
126
+ @color = color
127
+ @pixels = [@color] * (@width * @height)
128
+
129
+ # Extended features
130
+ if gce || delay || trans_color || disposal
131
+ @gce = gce ? gce.dup : Extension::GraphicControl.new
132
+ @gce.delay = delay if delay
133
+ @gce.trans_color = trans_color if trans_color
134
+ @gce.disposal = disposal if disposal
135
+ end
136
+ end
137
+
138
+ # Encode the image data to GIF format and write it to a stream.
139
+ # @param stream [IO] Stream to write the data to.
140
+ # @todo Add support for interlaced images.
141
+ def encode(stream)
142
+ # Optional Graphic Control Extension before image data
143
+ @gce.encode(stream) if @gce
144
+
145
+ # Image descriptor
146
+ stream << ','
147
+ stream << [@x, @y, @width, @height].pack('S<4')
148
+ flags = (@interlace ? 1 : 0) << 6
149
+ flags |= @lct.local_flags if @lct
150
+ stream << [flags].pack('C')
151
+
152
+ # Local Color Table
153
+ @lct.encode(stream) if @lct
154
+
155
+ # LZW-compressed image data
156
+ min_bits = 8 #@lct ? @lct.bit_size : 8
157
+ stream << min_bits.chr
158
+ lzw = LZWrb.new(preset: LZWrb::PRESET_GIF, min_bits: min_bits, verbosity: :minimal)
159
+ stream << Util.blockify(lzw.encode(@pixels.pack('C*')))
160
+ end
161
+
162
+ # Create a duplicate copy of this image.
163
+ # @return [Image] The new image.
164
+ def dup
165
+ lct = @lct ? @lct.dup : nil
166
+ gce = @gce ? @gce.dup : nil
167
+ image = Image.new(
168
+ @width, @height, @x, @y,
169
+ color: @color, gce: gce, delay: @delay, trans_color: @trans_color,
170
+ disposal: @disposal, interlace: @interlace, lct: lct
171
+ )
172
+ image
173
+ end
174
+
175
+ # Get current delay, in 1/100ths of a second, to display this image before
176
+ # moving on to the next one. Note that very small delays are typically not
177
+ # supported, see {Extension::GraphicControl#delay} for more details.
178
+ # @return [Integer] Time to display the image.
179
+ # @see Extension::GraphicControl#delay
180
+ def delay
181
+ @gce ? @gce.delay : nil
182
+ end
183
+
184
+ # Set current delay, in 1/100ths of a second, to display this image before
185
+ # moving on to the next one. Note that very small delays are typically not
186
+ # supported, see {Extension::GraphicControl#delay} for more details.
187
+ # @return (see #delay)
188
+ # @see (see #delay)
189
+ def delay=(value)
190
+ @gce = Extension::GraphicControl.new if !@gce
191
+ @gce.delay = value
192
+ end
193
+
194
+ # Get the disposal method of the image, which specifies how to handle the
195
+ # disposal of this image before displaying the next one in the GIF. See
196
+ # {Extension::GraphicControl#disposal} for details about the
197
+ # different disposal methods available.
198
+ # @return [Integer] The current disposal method.
199
+ # @see Extension::GraphicControl#disposal
200
+ def disposal
201
+ @gce ? @gce.disposal : nil
202
+ end
203
+
204
+ # Set the disposal method of the image, which specifies how to handle the
205
+ # disposal of this image before displaying the next one in the GIF. See
206
+ # {Extension::GraphicControl#disposal} for details about the
207
+ # different disposal methods available.
208
+ # @return (see #disposal)
209
+ # @see (see #disposal)
210
+ def disposal=(value)
211
+ @gce = Extension::GraphicControl.new if !@gce
212
+ @gce.disposal = value
213
+ end
214
+
215
+ # Get the index (in the color table) of the transparent color. Pixels with
216
+ # this color aren't rendered, and instead the background shows through them.
217
+ # See {Extension::GraphicControl#trans_color} for more details.
218
+ # @return [Integer] Index of the transparent color.
219
+ # @see Extension::GraphicControl#trans_color
220
+ def trans_color
221
+ @gce ? @gce.trans_color : nil
222
+ end
223
+
224
+ # Set the index (in the color table) of the transparent color. Pixels with
225
+ # this color aren't rendered, and instead the background shows through them.
226
+ # See {Extension::GraphicControl#trans_color} for more details.
227
+ # @return (see #trans_color)
228
+ # @see (see #trans_color)
229
+ def trans_color=(value)
230
+ @gce = Extension::GraphicControl.new if !@gce
231
+ @gce.trans_color = value
232
+ end
233
+
234
+ # Change the pixel data (color indices) of the image. The size of the array
235
+ # must match the current dimensions of the canvas, otherwise a manual resize
236
+ # is first required.
237
+ # @param pixels [Array<Integer>] The new pixel data to fill the canvas.
238
+ # @raise [Exception::CanvasError] If the supplied pixel data length doesn't match the
239
+ # canvas's current dimensions.
240
+ # @return (see #initialize)
241
+ def replace(pixels)
242
+ if pixels.size != @width * @height
243
+ raise Exception::CanvasError, "Pixel data doesn't match image dimensions. Please\
244
+ resize the image first."
245
+ end
246
+ @pixels = pixels
247
+ self
248
+ end
249
+
250
+ # Change the image's width and height. If the provided values are smaller,
251
+ # the image is cropped. If they are larger, the image is padded with the
252
+ # color specified by {#color}.
253
+ # @return (see #initialize)
254
+ def resize(width, height)
255
+ @pixels = @pixels.each_slice(@width).map{ |row|
256
+ width > @width ? row + [@color] * (width - @width) : row.take(width)
257
+ }
258
+ @pixels = height > @height ? @pixels + ([@color] * width) * (height - @height) : @pixels.take(height)
259
+ @pixels.flatten!
260
+ self
261
+ end
262
+
263
+ # Place the image at a different origin of coordinates.
264
+ # @param x [Integer] New origin horizontal coordinate.
265
+ # @param y [Integer] New origin vertical coordinate.
266
+ # @return (see #initialize)
267
+ # @see #move
268
+ # @raise [Exception::CanvasError] If we're placing the image out of bounds.
269
+ # @todo We're only checking negative out of bounds, what about positive ones?
270
+ def place(x, y)
271
+ raise Exception::CanvasError, "Cannot move image, out of bounds." if @x < 0 || @y < 0
272
+ @x = x
273
+ @y = y
274
+ self
275
+ end
276
+
277
+ # Move the image relative to the current position.
278
+ # @param x [Integer] X displacement.
279
+ # @param y [Integer] Y displacement.
280
+ # @return (see #initialize)
281
+ # @see #place
282
+ # @raise [Exception::CanvasError] If the movement would place the image out of bounds.
283
+ # @todo We're only checking negative out of bounds, what about positive ones?
284
+ def move(x, y)
285
+ raise Exception::CanvasError, "Cannot move image, out of bounds." if @x < -x || @y < -y
286
+ @x += x
287
+ @y += y
288
+ self
289
+ end
290
+
291
+ # Returns the bounding box of the image. This is a tuple of the form
292
+ # `[X, Y, W, H]`, where `[X, Y]` are the coordinates of its upper left
293
+ # corner - i.e., it's offset in the logical screen - and `[W, H]` are
294
+ # its width and height, respectively, in pixels.
295
+ # @return [Array] The image's bounding box in the format described above.
296
+ def bbox
297
+ [@x, @y, @width, @height]
298
+ end
299
+
300
+ # Get the value (color _index_) of a pixel fast (i.e. without bound checks).
301
+ # See also {#get}.
302
+ # @param x [Integer] The X coordinate of the pixel.
303
+ # @param y [Integer] The Y coordinate of the pixel.
304
+ # @return [Integer] The color index of the pixel.
305
+ def [](x, y)
306
+ @pixels[y * width + x]
307
+ end
308
+
309
+ # Set the value (color _index_) of a pixel fast (i.e. without bound checks).
310
+ # See also {#set}.
311
+ # @param x [Integer] The X coordinate of the pixel.
312
+ # @param y [Integer] The Y coordinate of the pixel.
313
+ # @param color [Integer] The new color index of the pixel.
314
+ # @return [Integer] The new color index of the pixel.
315
+ def []=(x, y, color)
316
+ @pixels[y * width + x] = color & 0xFF
317
+ end
318
+
319
+ # Get the values (color _index_) of a list of pixels safely (i.e. with bound
320
+ # checks). For the fast version, see {#[]}.
321
+ # @param points [Array<Array<Integer>>] The list of points whose color should
322
+ # be retrieved. Must be an array of pairs of coordinates.
323
+ # @return [Array<Integer>] The list of colors, in the same order.
324
+ # @raise [Exception::CanvasError] If any of the specified points is out of bounds.
325
+ def get(points)
326
+ bound_check(points.min_by(&:first)[0], points.min_by(&:last)[1])
327
+ bound_check(points.max_by(&:first)[0], points.max_by(&:last)[1])
328
+ points.map{ |p|
329
+ @pixels[p[1] * width + p[0]]
330
+ }
331
+ end
332
+
333
+ # Set the values (color _index_) of a list of pixels safely (i.e. with bound
334
+ # checks). For the fast version, see {#[]=}.
335
+ # @param points [Array<Array<Integer>>] The list of points whose color to
336
+ # change. Must be an array of pairs of coordinates.
337
+ # @param colors [Integer, Array<Integer>] The color(s) to assign. If an
338
+ # integer is passed, then all pixels will be set to the same color.
339
+ # Alternatively, an array with the same length as the points list must be
340
+ # passed, and each point will be set to the respective color in the list.
341
+ # @return (see #initialize)
342
+ # @raise [Exception::CanvasError] If any of the specified points is out of bounds.
343
+ def set(points, colors)
344
+ bound_check(points.min_by(&:first)[0], points.min_by(&:last)[1])
345
+ bound_check(points.max_by(&:first)[0], points.max_by(&:last)[1])
346
+ single = colors.is_a?(Integer)
347
+ points.each_with_index{ |p, i|
348
+ @pixels[p[1] * width + p[0]] = single ? color & 0xFF : colors[i] & 0xFF
349
+ }
350
+ self
351
+ end
352
+
353
+ # Draw a straight line connecting 2 points. It requires the startpoint `p1`
354
+ # and _either_ of the following:
355
+ # * The endpoint (`p2`).
356
+ # * The displacement vector (`vector`).
357
+ # * The direction vector (`direction`) and the length (`length`).
358
+ # * The angle (`angle`) and the length (`length`).
359
+ # @param p1 [Array<Integer>] The [X, Y] coordinates of the startpoint.
360
+ # @param p2 [Array<Integer>] The [X, Y] coordinates of the endpoint.
361
+ # @param vector [Array<Integer>] The coordinates of the displacement vector.
362
+ # @param direction [Array<Integer>] The coordinates of the direction vector.
363
+ # If this method is chosen, the `length` must be provided as well.
364
+ # Note that this vector will be normalized automatically.
365
+ # @param angle [Float] Angle of the line in radians (0-2Pi).
366
+ # If this method is chosen, the `length` must be provided as well.
367
+ # @param length [Float] The length of the line. Must be provided if either
368
+ # the `direction` or the `angle` method is being used.
369
+ # @param color [Integer] Index of the color of the line.
370
+ # @param weight [Integer] Width of the line in pixels.
371
+ # @param tip [Boolean] Whether to include the line's final pixel. This
372
+ # is useful for when multiple lines are joined to form a polygonal line.
373
+ # @return (see #initialize)
374
+ # @raise [Exception::CanvasError] If the line would go out of bounds.
375
+ # @todo Add support for anchors and anti-aliasing, better brushes, etc.
376
+ def line(p1: nil, p2: nil, vector: nil, angle: nil,
377
+ direction: nil, length: nil, color: 0, weight: 1, tip: true)
378
+ # Determine start and end points
379
+ raise Exception::CanvasError, "The line start must be specified." if !p1
380
+ x0, y0 = p1
381
+ if p2
382
+ x1, y1 = p2
383
+ else
384
+ x1, y1 = Geometry.endpoint(
385
+ point: p1, vector: vector, direction: direction,
386
+ angle: angle, length: length
387
+ )
388
+ end
389
+ bound_check(x0, y0)
390
+ bound_check(x1, y1)
391
+
392
+ # Normalize coordinates
393
+ swap = (y1 - y0).abs > (x1 - x0).abs
394
+ x0, x1, y0, y1 = y0, y1, x0, x1 if swap
395
+
396
+ # Precompute useful parameters
397
+ dx = x1 - x0
398
+ dy = y1 - y0
399
+ sx = dx < 0 ? -1 : 1
400
+ sy = dy < 0 ? -1 : 1
401
+ dx *= sx
402
+ dy *= sy
403
+ x, y = x0, y0
404
+ s = [sx, sy]
405
+
406
+ # If both endpoints are the same, draw a single point and return
407
+ if dx == 0
408
+ line_chunk(x0, y0, color, weight, swap)
409
+ return self
410
+ end
411
+
412
+ # Rotate weight
413
+ e_acc = 0
414
+ e = ((dy << 16) / dx.to_f).round
415
+ max = (dy <= 1 ? dx - 1 : dx + (weight / 2.0).to_i - 2) + (tip ? 1 : 0)
416
+ weight *= (1 + (dy.to_f / dx) ** 2) ** 0.5
417
+
418
+ # Draw line chunks
419
+ 0.upto(max) do |i|
420
+ e_acc += e
421
+ y += sy if e_acc > 0xFFFF unless i == 0 && e == 0x10000
422
+ e_acc &= 0xFFFF
423
+ w = 0xFF - (e_acc >> 8)
424
+ brush_chunk(x, y, color, weight, swap)
425
+ x += sx
426
+ end
427
+
428
+ self
429
+ end
430
+
431
+ # Draw a rectangle with border and optional fill.
432
+ # @param x [Integer] X coordinate of the top-left vertex.
433
+ # @param y [Integer] Y coordinate of the top-left vertex.
434
+ # @param w [Integer] Width of the rectangle in pixels.
435
+ # @param h [Integer] Height of the rectangle in pixels.
436
+ # @param stroke [Integer] Index of the border color.
437
+ # @param fill [Integer] Index of the fill color (`nil` for no fill).
438
+ # @param weight [Integer] Stroke width of the border in pixels.
439
+ # @return (see #initialize)
440
+ # @raise [Exception::CanvasError] If the rectangle would go out of bounds.
441
+ def rect(x, y, w, h, stroke, fill = nil, weight: 1)
442
+ # Check coordinates
443
+ x0, y0, x1, y1 = x, y, x + w - 1, y + h - 1
444
+ bound_check(x0, y0)
445
+ bound_check(x1, y1)
446
+
447
+ # Fill rectangle, if provided
448
+ if fill
449
+ for x in (x0 .. x1)
450
+ for y in (y0 .. y1)
451
+ @pixels[y * @width + x] = fill
452
+ end
453
+ end
454
+ end
455
+
456
+ # Rectangle border
457
+ line(p1: [x0, y0], p2: [x1, y0], color: stroke, weight: weight)
458
+ line(p1: [x0, y1], p2: [x1, y1], color: stroke, weight: weight)
459
+ line(p1: [x0, y0], p2: [x0, y1], color: stroke, weight: weight)
460
+ line(p1: [x1, y0], p2: [x1, y1], color: stroke, weight: weight)
461
+
462
+ self
463
+ end
464
+
465
+ private
466
+
467
+ # Ensure the provided point is within the image's bounds.
468
+ def bound_check(x, y)
469
+ Geometry.bound_check([[x, y]], [0, 0, @width, @height])
470
+ end
471
+
472
+ # Draw one line chunk, used for Xiaolin-Wu line algorithm
473
+ def line_chunk(x, y, color, weight = 1, swap = false)
474
+ weight = 1.0 if weight < 1.0
475
+ weight = weight.to_i
476
+ min = - weight / 2 + 1
477
+ max = weight / 2
478
+
479
+ w = min
480
+ if !swap
481
+ self[x, y + w] = color
482
+ self[x, y + w] = color while (w += 1) < max
483
+ self[x, y + w] = color if w <= max
484
+ else
485
+ self[y + w, x] = color
486
+ self[y + w, x] = color while (w += 1) < max
487
+ self[y + w, x] = color if w <= max
488
+ end
489
+ end
490
+
491
+ def brush_chunk(x, y, color, weight = 1, swap = false)
492
+ weight = 1.0 if weight < 1.0
493
+ weight = weight.to_i
494
+ x, y = y, x if swap
495
+ cross_brush = [
496
+ [ 0, -1],
497
+ [-1, 0],
498
+ [ 0, 0],
499
+ [ 1, 0],
500
+ [ 0, 1]
501
+ ]
502
+ two_by_two_brush = [
503
+ [0, 0],
504
+ [1, 0],
505
+ [0, 1],
506
+ [1, 1]
507
+ ]
508
+ two_by_two_brush.each{ |dx, dy| self[x + dx, y + dy] = color }
509
+ end
510
+
511
+ end
512
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,46 @@
1
+ module Gifenc
2
+
3
+ # Encapsulates generic functionality that is useful when handling GIF files.
4
+ module Util
5
+
6
+ # Divide data block into a series of sub-blocks of size at most 256 bytes each,
7
+ # consisting on a 1-byte prefix indicating the block length, and <255 bytes of
8
+ # actual data, with a null terminator at the end. This is how raw data (e.g.
9
+ # compressed pixel data or extension data) is stored in GIFs.
10
+ # @param data [String] Data to lay into sub-blocks.
11
+ # @return [String] The resulting data in block fashion.
12
+ def self.blockify(data)
13
+ return BLOCK_TERMINATOR if data.size == 0
14
+ ff = "\xFF".b.freeze
15
+ off = 0
16
+ out = "".b
17
+ len = data.length
18
+ for _ in (0 ... len / 255)
19
+ out << ff << data[off ... off + 255]
20
+ off += 255
21
+ end
22
+ out << (len - off).chr << data[off..-1] if off < len
23
+ out << BLOCK_TERMINATOR
24
+ out
25
+ rescue
26
+ BLOCK_TERMINATOR
27
+ end
28
+
29
+ # Recover original data from inside the 256-byte blocks used by GIF.
30
+ # @param data [String] Data in blocks to read.
31
+ # @return [String] Original raw data.
32
+ def self.deblockify(data)
33
+ out = ""
34
+ size = data[0].ord
35
+ off = 0
36
+ while size != 0
37
+ out << data[off + 1 .. off + size]
38
+ off += size + 1
39
+ size = data[off].ord
40
+ end
41
+ out
42
+ rescue
43
+ ''.b
44
+ end
45
+ end
46
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gifenc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - edelkas
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-02-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lzwrb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: |2
28
+ This library provides GIF encoding, decoding and editing capabilities natively
29
+ within Ruby. It aims to support the complete GIF specification for both
30
+ encoding and decoding, as well as decent editing functionality, while
31
+ maintaining a succint syntax.
32
+
33
+ The current version is still preliminar, and only encoding is working,
34
+ but the gem is actively developed and decoding will soon follow, so stay
35
+ tuned if you're interested!
36
+ email:
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files:
40
+ - README.md
41
+ - docs/Specification.md
42
+ files:
43
+ - ".yardopts"
44
+ - README.md
45
+ - docs/Specification.md
46
+ - lib/color_table.rb
47
+ - lib/errors.rb
48
+ - lib/extensions.rb
49
+ - lib/geometry.rb
50
+ - lib/gif.rb
51
+ - lib/gifenc.rb
52
+ - lib/image.rb
53
+ - lib/util.rb
54
+ homepage: https://github.com/edelkas/gifenc
55
+ licenses: []
56
+ metadata:
57
+ homepage_uri: https://github.com/edelkas/gifenc
58
+ source_code_uri: https://github.com/edelkas/gifenc
59
+ documentation_uri: https://www.rubydoc.info/gems/gifenc/
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.1.6
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: GIF encoder, decoder and editor in pure Ruby
79
+ test_files: []