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.
- checksums.yaml +7 -0
- data/.yardopts +1 -0
- data/README.md +69 -0
- data/docs/Specification.md +2477 -0
- data/lib/color_table.rb +306 -0
- data/lib/errors.rb +24 -0
- data/lib/extensions.rb +275 -0
- data/lib/geometry.rb +118 -0
- data/lib/gif.rb +228 -0
- data/lib/gifenc.rb +60 -0
- data/lib/image.rb +512 -0
- data/lib/util.rb +46 -0
- metadata +79 -0
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: []
|