gifenc 0.2.0 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f67cc6f62dedeaa913d3de8a8729ff5af750799a5d9bbbd8b9693092fadc5ee
4
- data.tar.gz: 7b22b1ca32a728372175f58affa8eeeaf495eff2f6357557b88554406ab4e375
3
+ metadata.gz: 5c38b840026c2289d2524c5e06bf2d1943b468662bd45a0f7abe456a6405badc
4
+ data.tar.gz: fc17037791eff68a2fec84b7689b082726e51aa1363b1e94885a9535f4b37db1
5
5
  SHA512:
6
- metadata.gz: 14483dee19a0cfdc922d14f90816662ee13aeccbe78b247f69c84f805be243799c74141a169610f9420a935143744a85325e9ea57fa344e43c37e27631d3387d
7
- data.tar.gz: 31a19387a6f2581f54ba6c1ad2fca893021b3e82c55d092adab6e274214f613fc3ccad52158e93c2dcdb51f4c9d1cfa7b839750af20949967cf5bc1bfca8a78f
6
+ metadata.gz: 54367e7f3751f0370fe6e4a297fb29b323a25d25ad9ef0e9a435ec8df3cc6010ff2f012d41cda0195cb8a42456069e15a03f3fdfd78f0990be1ecb78fd5f35f9
7
+ data.tar.gz: 12e4facd900f5628554bfc479044ffe33df18dfaf365586dbc259eeace2e643c9cecb6b187c7276f0eba64b04c1e4f987486635d0e4bae0d2572453d9b795962
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ### 0.2.1 (18/Mar/2024)
4
+
5
+ Improved the image copy method. It now automatically corrects both source and destination offsets / dimensions to prevent any out of bounds errors, allows to specify a bounding box to which the copying should be restricted to, and allows to "copy with transparency", which will copy every color except for the transparent one.
6
+
3
7
  ### 0.2.0 (05/Mar/2024)
4
8
 
5
9
  A big update, with the main changes being divided in two categories: mathematical methods and drawing methods.
@@ -10,7 +14,7 @@ A big update, with the main changes being divided in two categories: mathematica
10
14
  * Point operations, such as translations, dilations / scalings, linear and convex combinations, scalar product, center of mass, etc.
11
15
  * Point transformations, such as rotations, projections and reflections.
12
16
  * Norms (L1, euclidean, supremum), normalization, distances.
13
- * Angles, parallelism, orthonality, normal vectors...
17
+ * Angles, parallelism, orthogonality, normal vectors...
14
18
 
15
19
  - Significantly expanded the drawing functionality, including:
16
20
  * Improved line drawing, and added line styles (dashed, dotted, etc).
@@ -18,10 +22,11 @@ A big update, with the main changes being divided in two categories: mathematica
18
22
  * Added grids, polygonal chains and spirals.
19
23
  * Arbitrary parametric curves and function graphs given a lambda function.
20
24
  * Implemented the flood fill / bucket tool.
25
+ * Added a general Brush class to enable more flexible drawing.
21
26
 
22
27
  Many sample GIFs, showcasing all this new functionality, were added in the [Examples](https://github.com/edelkas/gifenc/tree/master/examples) folder as well.
23
28
 
24
- - Other functionality added includes:
29
+ - Additional new functionality includes:
25
30
  * Copy: Ability to copy a region from one image to another.
26
31
  * Compress: Ability to LZW-compress image data on the fly, instead of keeping
27
32
  everything raw in memory until final encode time. Particularly helpful for
data/README.md CHANGED
@@ -11,7 +11,7 @@ A pure Ruby library with no external dependencies that allows to encode, decode
11
11
  * Have a decent suite of editing functionalities, so that the need for external tools is avoided as much as possible.
12
12
  * Have a succint and comfortable syntax to use.
13
13
 
14
- Currently, the specs are almost fully supported for encoding. Decoding is not yet available, and the editing methods are very limited. See the [Reference](https://www.rubydoc.info/gems/gifenc) for the full documentation, as well as [Examples](https://github.com/edelkas/gifenc/tree/master/examples) for a list of sample snippets and GIFs.
14
+ Currently, the specs are almost fully supported for encoding. Decoding is not yet available, but will be soon. There's a solid `Geometry` module and decent drawing functionality. See the [Reference](https://www.rubydoc.info/gems/gifenc) for the full documentation, as well as [Examples](https://github.com/edelkas/gifenc/tree/master/examples) for a list of sample snippets and GIFs.
15
15
 
16
16
  ## A first example
17
17
 
@@ -51,7 +51,7 @@ end
51
51
  gif.save('test.gif')
52
52
  ```
53
53
 
54
- Let's see a step-by-step overview, refer to the following sections for an in-depth explanation of the actual details for each of the topics involved.
54
+ Let's see a step-by-step overview, refer to the documentation for an in-depth explanation of the actual details for each of the topics involved.
55
55
  1. The first thing we do is create two **Color Tables**, one with red shades, and another with green shades. Since GIF is an indexed image format, it can only use colors from predefined palettes of at most 256 colors. `Gifenc` comes equipped with several default ones, but you can build your own, and operate with them.
56
56
  2. We create the GIF object. We select the red color table to be the **GCT** (_Global Color Table_), which is used for all frames that do not contain an explicit **LCT** (_Local Color Table_). We also set the GIF to loop indefinitely.
57
57
  3. We create the first frame, this will act as the background. We use the green color table as LCT for this frame. We set a few attributes, such as the default color of the canvas, the length of the frame, and the color used for transparency. We draw a sequence of centered green squares, they will help to see the transparency of the next frames.
data/lib/geometry.rb CHANGED
@@ -542,6 +542,26 @@ module Gifenc
542
542
  [x0, y0, x1 - x0 + 1, y1 - y0 + 1]
543
543
  end
544
544
 
545
+ # Calculate the intersection of multiple rectangles. The rectangles must be
546
+ # supplied in the usual format of bounding boxes, i.e. `[X, Y, W, H]`,
547
+ # where `X` and `Y` are the coordinates of the upper left corner of the
548
+ # rectangle with respect to the image, and `W` and `H` are the width and
549
+ # height of the rectangle, respectively. The result will be a rectangle
550
+ # in the same format, if the intersection is non-empty, or `nil` otherwise.
551
+ # @param rects [Array<Array<Float>>] The rectangles to intersect.
552
+ # @return [Array<Float>,nil] The resulting intersection.
553
+ def self.rect_overlap(*rects)
554
+ return if rects.empty?
555
+
556
+ rect = rects[0]
557
+ rects[1..-1].each{ |r|
558
+ rect = rect_overlap_two(rect, r)
559
+ return if !rect
560
+ }
561
+
562
+ rect
563
+ end
564
+
545
565
  # Translate a set of points according to a fixed vector. Given a list of
546
566
  # points `P1, ... , Pn` and a translation vector `t`, this method will
547
567
  # return the transformed list of points `P1 + t, ... , Pn + t`.
@@ -673,7 +693,8 @@ module Gifenc
673
693
  # Find the center of mass (barycenter) of a list of points. This will always
674
694
  # be contained within the convex hull of the supplied points.
675
695
  # @param points [Array<Point>] The list of points.
676
- # @return [Point]
696
+ # @return [Point] The center of mass of the points.
697
+ # @raise [Exception::GeometryError] If the list of points is empty.
677
698
  def self.center(points)
678
699
  raise Exception::GeometryError, "Cannot find center of empty list of points." if points.size == 0
679
700
  points.map!{ |p| Point.parse(p) }
@@ -690,5 +711,23 @@ module Gifenc
690
711
  (r.y - p.y) * (q.x - p.x) - (q.y - p.y) * (r.x - p.x)
691
712
  end
692
713
 
714
+ # Find overlap of 2 rectangles. Used recursively by rect_overlap to find
715
+ # overlap of N rectangles.
716
+ def self.rect_overlap_two(rect1, rect2)
717
+ return nil if !rect1 || !rect2
718
+
719
+ x_max = [rect1[0], rect2[0]].max
720
+ y_max = [rect1[1], rect2[1]].max
721
+ x_min = [rect1[0] + rect1[2], rect2[0] + rect2[2]].min
722
+ y_min = [rect1[1] + rect1[3], rect2[1] + rect2[3]].min
723
+
724
+ x = x_max
725
+ y = y_max
726
+ w = x_min - x_max
727
+ h = y_min - y_max
728
+
729
+ [w, h].min > PRECISION ? [x, y, w, h] : nil
730
+ end
731
+
693
732
  end # Module Geometry
694
733
  end # Module Gifenc
data/lib/gif.rb CHANGED
@@ -177,7 +177,7 @@ module Gifenc
177
177
  @images.size.times.each{ |i|
178
178
  @images[i].encode(stream)
179
179
  if @destroy
180
- @images[i].clear
180
+ @images[i].destroy
181
181
  @images[i] = nil
182
182
  end
183
183
  }
data/lib/image.rb CHANGED
@@ -186,6 +186,7 @@ module Gifenc
186
186
  # @return (see #delay)
187
187
  # @see (see #delay)
188
188
  def delay=(value)
189
+ return if !value
189
190
  @gce = Extension::GraphicControl.new if !@gce
190
191
  @gce.delay = value
191
192
  end
@@ -207,6 +208,7 @@ module Gifenc
207
208
  # @return (see #disposal)
208
209
  # @see (see #disposal)
209
210
  def disposal=(value)
211
+ return if !value
210
212
  @gce = Extension::GraphicControl.new if !@gce
211
213
  @gce.disposal = value
212
214
  end
@@ -226,6 +228,7 @@ module Gifenc
226
228
  # @return (see #trans_color)
227
229
  # @see (see #trans_color)
228
230
  def trans_color=(value)
231
+ return if !value
229
232
  @gce = Extension::GraphicControl.new if !@gce
230
233
  @gce.trans_color = value
231
234
  end
@@ -268,49 +271,105 @@ module Gifenc
268
271
  self
269
272
  end
270
273
 
271
- # Clear the pixel data of the image. This simply substitutes the contents
274
+ # Destroy the pixel data of the image. This simply substitutes the contents
272
275
  # of the array, hoping that the underlying data will go out of scope and
273
276
  # be collected by the garbage collector. This is intended for freeing
274
277
  # space and to simulate "destroying" the image.
275
278
  # @return (see #initialize)
276
- def clear
279
+ def destroy
277
280
  @pixels = nil
278
281
  self
279
282
  end
280
283
 
281
- # Copy a rectangular region from another image to this one. The offsets and
282
- # dimensions of the region can be specified.
284
+ # Paint the whole canvas with the base image color.
285
+ # @return (see #initialize)
286
+ def clear
287
+ @pixels = [@color] * (@width * @height)
288
+ self
289
+ end
290
+
291
+ # Copy a rectangular region from another image to this one. The dimension of
292
+ # the region, as well as the source offset and the destination offset, can
293
+ # be provided. If the region would go out of bounds, the function will
294
+ # just gracefully crop it rather than failing. Additionally, a more restrictive
295
+ # bounding box (smaller than the full image) can also be provided, to where
296
+ # the copying will be confined.
283
297
  # @note The two images are assumed to have the same color table, since what
284
298
  # is copied is the color indexes.
285
- # @param source [Image] The source image to copy the contents from.
299
+ # @param src [Image] The source image to copy the contents from.
286
300
  # @param offset [Array<Integer>] The coordinates of the offset of the region
287
301
  # in the source image.
288
302
  # @param dim [Array<Integer>] The dimensions of the region, in the form `[W, H]`,
289
- # where W is the width and H is the height of the rectangle to copy.
303
+ # where W is the width and H is the height of the rectangle to copy. If
304
+ # unspecified (`nil`), the whole source image will be copied.
290
305
  # @param dest [Array<Integer>] The coordinates of the destination offset of
291
306
  # the region in this image.
292
- # @raise [Exception::CanvasError] If the region is out of bounds in either
293
- # the source or the destination images.
307
+ # @param trans [Boolean] If enabled, the pixels from the source image whose
308
+ # color is the transparent one (for the source image) won't be copied over,
309
+ # effectively achieving the usual GIF composition with basic transparency.
310
+ # It's a bit slower as a consequence.
311
+ # @param bbox [Array<Integer>] The bounding box (with respect to this image)
312
+ # where the copying should be restricted to. Everything outside this region
313
+ # will be left untouched. If unspecified (`nil`), this will be the whole
314
+ # image. As usual, the format is `[X, Y, W, H]`, where `[X, Y]` are the
315
+ # coordinates of the upper left pixel, and `[W, H]` are the pixel width
316
+ # and height, respectively.
317
+ # @raise [Exception::CanvasError] If no source provided.
294
318
  # @return (see #initialize)
295
- def copy(source: nil, offset: [0, 0], dim: [1, 1], dest: [0, 0])
296
- offset = Geometry::Point.parse(offset)
297
- dim = Geometry::Point.parse(dim)
298
- dest = Geometry::Point.parse(dest)
299
- if !source.bound_check(offset) || !source.bound_check(offset + dim - [1, 1])
300
- raise Exception::CanvasError, "Cannot copy, region out of bounds in source image."
301
- end
302
- if !bound_check(dest) || !bound_check(dest + dim - [1, 1])
303
- raise Exception::CanvasError, "Cannot copy, region out of bounds in destination image."
319
+ def copy(src: nil, offset: [0, 0], dim: nil, dest: [0, 0], trans: false, bbox: nil)
320
+ raise Exception::CanvasError, "Cannot copy, no source provided." if !src
321
+
322
+ # Parse parameters
323
+ bbox = [0, 0, @width, @height] unless bbox
324
+ dim = [src.width, src.height] unless dim
325
+ offset = Geometry::Point.parse(offset).round
326
+ dim = Geometry::Point.parse(dim).round
327
+ dest = Geometry::Point.parse(dest).round
328
+
329
+ # Normalize main bbox
330
+ bbox = Geometry.rect_overlap(bbox, [0, 0, @width, @height])
331
+ return if !bbox
332
+ bbox.map!(&:round)
333
+
334
+ # Normalize source bbox
335
+ src_bbox = [offset.x, offset.y, dim.x, dim.y]
336
+ src_bbox = Geometry.rect_overlap(src_bbox, [0, 0, src.width, src.height])
337
+ return if !src_bbox
338
+ offset = Geometry::Point.parse(src_bbox[0, 2])
339
+ dim = Geometry::Point.parse(src_bbox[2, 2])
340
+
341
+ # Normalize destination bbox
342
+ dest_bbox = [dest.x, dest.y, dim.x, dim.y]
343
+ overlap = Geometry.rect_overlap(dest_bbox, bbox)
344
+ return if !dest_bbox
345
+ dest = Geometry::Point.parse(overlap[0, 2])
346
+ dim = Geometry::Point.parse(overlap[2, 2])
347
+
348
+ # Transform coordinates of source to coordinates of destination
349
+ offset += Gifenc::Geometry.transform([dest], dest_bbox)[0]
350
+
351
+ # Handy fetch
352
+ dx, dy = dest.x.round, dest.y.round
353
+ ox, oy = offset.x.round, offset.y.round
354
+ lx, ly = dim.x.round, dim.y.round
355
+ bg = src.trans_color
356
+
357
+ # Copy pixel data. We use a different, slightly faster, algorithm if we
358
+ # don't have a bg color check to make, by directly copying full rows.
359
+ if trans && bg
360
+ c = nil
361
+ ly.times.each{ |y|
362
+ lx.times.each{ |x|
363
+ c = src[ox + x, oy + y]
364
+ self[dx + x, dy + y] = c unless c == bg
365
+ }
366
+ }
367
+ else
368
+ ly.times.each{ |y|
369
+ @pixels[(dy + y) * @width + dx, lx] = src.pixels[(oy + y) * src.width + ox, lx]
370
+ }
304
371
  end
305
372
 
306
- dx = dest.x.round
307
- dy = dest.y.round
308
- ox = offset.x.round
309
- oy = offset.y.round
310
- dim.y.round.times.each{ |y|
311
- @pixels[(dy + y) * @width + dx, dim.x.round] = source.pixels[(oy + y) * source.width + ox, dim.x.round]
312
- }
313
-
314
373
  self
315
374
  end
316
375
 
@@ -459,9 +518,11 @@ module Gifenc
459
518
  # indicates the position of the line with respect to the coordinates. It
460
519
  # must be in the interval [-1, 1]. A value of `0` centers the line in its
461
520
  # width, a value of `-1` draws it on one side, and `1` on the other.
462
- # @param bbox [Array<Integer>] Bounding box determining the region in which
463
- # the line must be contained. Anything outside it won't be drawn. If
464
- # `nil`, this defaults to the whole image.
521
+ # @param bbox [Array<Integer>] Bounding box determining the region to which
522
+ # the drawing will be restricted, in the format `[X, Y, W, H]`, where `[X, Y]`
523
+ # are the coordinates of the upper left corner of the box, and `[W, H]` are
524
+ # the pixel dimensions. If unspecified (`nil`), this defaults to the whole
525
+ # image.
465
526
  # @param avoid [Array<Integer>] List of colors over which the line should
466
527
  # NOT be drawn.
467
528
  # @param style [Symbol] Named style / pattern of the line. Can be `:solid`
@@ -530,19 +591,29 @@ module Gifenc
530
591
  # * For `0` the border is centered around the boundary.
531
592
  # * For `1` the border is entirely contained within the boundary.
532
593
  # * For `-1` the border is entirely surrounding the boundary.
594
+ # @param style [Symbol] Border line style (`:solid`, `:dashed`, `:dotted`).
595
+ # See `style` param in {#line}).
596
+ # @param density [Symbol] Border line pattern density (`:normal`, `:dense`,
597
+ # `:loose`). See `density` param in {#line}.
598
+ # @param bbox [Array<Integer>] Bounding box determining the region to which
599
+ # the drawing will be restricted. See `bbox` param in {#line}.
533
600
  # @return (see #initialize)
534
601
  # @raise [Exception::CanvasError] If the rectangle would go out of bounds.
535
602
  def rect(x, y, w, h, stroke = nil, fill = nil, weight: 1, anchor: 1,
536
- style: :solid, density: :normal)
537
- # Check coordinates
538
- x = x.round
539
- y = y.round
540
- w = w.round
541
- h = h.round
542
- x0, y0, x1, y1 = x, y, x + w - 1, y + h - 1
543
- if !Geometry.bound_check([[x0, y0], [x1, y1]], self, true)
544
- raise Exception::CanvasError, "Rectangle out of bounds."
545
- end
603
+ style: :solid, density: :normal, bbox: nil)
604
+ # Normalize bbox
605
+ bbox = [0, 0, @width, @height] unless bbox
606
+ bbox = Geometry.rect_overlap(bbox, [0, 0, @width, @height])
607
+ return if !bbox
608
+ bbox.map!(&:round)
609
+
610
+ # Intersect bbox with rectangle
611
+ rect_bbox = Geometry.rect_overlap(bbox, [x, y, w, h])
612
+ return if !rect_bbox
613
+ x0 = rect_bbox[0].round
614
+ y0 = rect_bbox[1].round
615
+ x1 = (rect_bbox[0] + rect_bbox[2]).round - 1
616
+ y1 = (rect_bbox[1] + rect_bbox[3]).round - 1
546
617
 
547
618
  # Fill rectangle, if provided
548
619
  if fill
@@ -559,7 +630,7 @@ module Gifenc
559
630
  o = ((weight - 1) / 2.0 * anchor).round
560
631
  w -= 2 * o - (weight % 2 == 0 ? 1 : 0)
561
632
  h -= 2 * o - (weight % 2 == 0 ? 1 : 0)
562
- rect(x + o, y + o, w, h, stroke, weight: weight, anchor: 0)
633
+ rect(x0 + o, y0 + o, w, h, stroke, weight: weight, anchor: 0)
563
634
  else
564
635
  points = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]]
565
636
  4.times.each{ |i|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gifenc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - edelkas
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-05 00:00:00.000000000 Z
11
+ date: 2024-03-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lzwrb
@@ -27,12 +27,12 @@ dependencies:
27
27
  description: |2
28
28
  This library provides GIF encoding, decoding and editing capabilities natively
29
29
  within Ruby. It aims to support the complete GIF specification for both
30
- encoding and decoding, as well as having a decent editing functionality, while
30
+ encoding and decoding, as well as having decent editing functionality, while
31
31
  maintaining a succint syntax.
32
32
 
33
- The current version is still preliminar, and only encoding is working, together
34
- with a decent drawing suite. The gem is actively developed and decoding will
35
- soon follow, so stay tuned if you're interested!
33
+ The current version only supports encoding, together with a decent drawing suite
34
+ The gem is actively developed and decoding will soon follow, so stay tuned
35
+ if you're interested!
36
36
  email:
37
37
  executables: []
38
38
  extensions: []